From f00308b1f0ff3bcc0cf00f0a7484cfdfbdab5522 Mon Sep 17 00:00:00 2001 From: Damocles Date: Mon, 20 Apr 2026 22:01:48 +0200 Subject: [PATCH] rewrite reproducer as quickshell qml config with layer-shell surfaces --- flake.nix | 37 +++++------ test/screen-uaf-reproducer/CMakeLists.txt | 10 --- test/screen-uaf-reproducer/main.cpp | 81 ----------------------- test/screen-uaf-reproducer/shell.nix | 11 --- test/screen-uaf-reproducer/shell.qml | 51 ++++++++++++++ test/screen-uaf-reproducer/trigger.sh | 7 +- 6 files changed, 72 insertions(+), 125 deletions(-) delete mode 100644 test/screen-uaf-reproducer/CMakeLists.txt delete mode 100644 test/screen-uaf-reproducer/main.cpp delete mode 100644 test/screen-uaf-reproducer/shell.nix create mode 100644 test/screen-uaf-reproducer/shell.qml diff --git a/flake.nix b/flake.nix index 5365927..1e7bd56 100644 --- a/flake.nix +++ b/flake.nix @@ -89,28 +89,21 @@ docs = pkgs.callPackage ./nix/docs.nix { inherit self; }; default = nova-shell; - # Reproducer for the qtbase wayland screen UAF, built twice: - # patched (should survive) and unpatched (should crash). - screen-uaf-reproducer-patched = pkgs.stdenv.mkDerivation { - pname = "screen-uaf-reproducer-patched"; - version = "0"; - src = ./test/screen-uaf-reproducer; - nativeBuildInputs = [ - pkgs.cmake - pkgs.qt6.wrapQtAppsHook - ]; - buildInputs = [ pkgs.qt6.qtbase ]; - }; - screen-uaf-reproducer-unpatched = rawPkgs.stdenv.mkDerivation { - pname = "screen-uaf-reproducer-unpatched"; - version = "0"; - src = ./test/screen-uaf-reproducer; - nativeBuildInputs = [ - rawPkgs.cmake - rawPkgs.qt6.wrapQtAppsHook - ]; - buildInputs = [ rawPkgs.qt6.qtbase ]; - }; + # Reproducer wrappers: run test/screen-uaf-reproducer/shell.qml + # with patched vs unpatched quickshell to confirm the fix. + screen-uaf-reproducer-patched = pkgs.writeShellScriptBin "screen-uaf-reproducer-patched" '' + exec ${qs}/bin/quickshell -p ${./test/screen-uaf-reproducer/shell.qml} + ''; + screen-uaf-reproducer-unpatched = + let + qsUnpatched = (rawPkgs.extend quickshell.overlays.default).quickshell.override { + withX11 = false; + withI3 = false; + }; + in + rawPkgs.writeShellScriptBin "screen-uaf-reproducer-unpatched" '' + exec ${qsUnpatched}/bin/quickshell -p ${./test/screen-uaf-reproducer/shell.qml} + ''; } ); diff --git a/test/screen-uaf-reproducer/CMakeLists.txt b/test/screen-uaf-reproducer/CMakeLists.txt deleted file mode 100644 index 6c9f356..0000000 --- a/test/screen-uaf-reproducer/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -cmake_minimum_required(VERSION 3.19) -project(screen-uaf-reproducer LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_AUTOMOC ON) - -find_package(Qt6 REQUIRED COMPONENTS Gui) -add_executable(screen-uaf-reproducer main.cpp) -target_link_libraries(screen-uaf-reproducer PRIVATE Qt6::Gui) -install(TARGETS screen-uaf-reproducer) diff --git a/test/screen-uaf-reproducer/main.cpp b/test/screen-uaf-reproducer/main.cpp deleted file mode 100644 index 73e5bc2..0000000 --- a/test/screen-uaf-reproducer/main.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// Minimal reproducer for Qt 6 Wayland screen use-after-free. -// -// Bug: libwayland resolves wl_output proxy pointers at demarshal time (when -// events are read from the socket). If wl_registry.global_remove and -// wl_surface.enter/leave for the same output arrive in the same dispatch -// batch, the surface event handler receives a dangling proxy pointer. -// QWaylandScreen::fromWlOutput() reads stale userdata from the freed proxy -// and returns a garbage QWaylandScreen*, which crashes on dereference. -// -// This reproducer simulates the pattern that triggers the crash in practice: -// multiple surfaces bound to an output that toggle visibility during screen -// changes (like layer-shell panel windows being hidden/reshown on output -// reconfiguration). Hiding a QWindow destroys its wl_surface; showing it -// creates a new one. This surface churn during output removal maximizes the -// chance that enter/leave events land in the same batch as global_remove. -// -// Build: -// cmake -B build && cmake --build build -// -// Run (on any wlroots-based compositor or Niri): -// ./build/screen-uaf-reproducer & -// # then toggle an output off/on, e.g.: -// # wlr-randr --output eDP-1 --off && wlr-randr --output eDP-1 --on -// # niri msg action power-off-monitors (then move mouse to wake) -// # or just unplug/replug an external monitor - -#include -#include -#include -#include -#include -#include - -static std::vector> windows; - -// Hide and immediately re-show all windows, destroying and recreating their -// wl_surfaces. This is what layer-shell panels do during output changes. -static void toggleAllWindows() -{ - std::fprintf(stderr, " toggling %zu windows...\n", windows.size()); - for (auto &w : windows) - w->hide(); - for (auto &w : windows) - w->show(); -} - -int main(int argc, char *argv[]) -{ - QGuiApplication app(argc, argv); - - constexpr int kWindows = 20; - windows.reserve(kWindows); - - for (int i = 0; i < kWindows; ++i) { - auto w = std::make_unique(); - w->setTitle(QStringLiteral("uaf-%1").arg(i)); - w->resize(100, 30); - w->setPosition(0, i * 35); - w->show(); - windows.push_back(std::move(w)); - } - - std::fprintf(stderr, - "screen-uaf-reproducer: %d windows on %d screen(s).\n" - "Toggle an output off/on to trigger the crash.\n", - kWindows, static_cast(QGuiApplication::screens().size())); - - // When screens change, toggle all windows - this creates surface - // destruction/creation right when the compositor is sending - // enter/leave/global_remove events, maximizing batch collision odds. - QObject::connect(&app, &QGuiApplication::screenAdded, [](QScreen *s) { - std::fprintf(stderr, " screenAdded: %s\n", qPrintable(s->name())); - toggleAllWindows(); - }); - QObject::connect(&app, &QGuiApplication::screenRemoved, [](QScreen *s) { - std::fprintf(stderr, " screenRemoved: %s\n", qPrintable(s->name())); - toggleAllWindows(); - }); - - return app.exec(); -} diff --git a/test/screen-uaf-reproducer/shell.nix b/test/screen-uaf-reproducer/shell.nix deleted file mode 100644 index 0c6fd61..0000000 --- a/test/screen-uaf-reproducer/shell.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ - pkgs ? import { }, -}: -pkgs.mkShell { - nativeBuildInputs = with pkgs; [ - cmake - qt6.qtbase - qt6.wrapQtAppsHook - wlr-randr - ]; -} diff --git a/test/screen-uaf-reproducer/shell.qml b/test/screen-uaf-reproducer/shell.qml new file mode 100644 index 0000000..9130ba0 --- /dev/null +++ b/test/screen-uaf-reproducer/shell.qml @@ -0,0 +1,51 @@ +// Minimal reproducer for Qt 6 Wayland screen use-after-free (QTBUG-XXXXXX). +// +// Creates many layer-shell surfaces bound to each screen. When an output is +// removed, the compositor sends wl_surface.leave + wl_registry.global_remove +// in the same event batch. libwayland resolves the wl_output proxy pointer at +// demarshal time; if the global_remove handler destroys the proxy first, the +// surface.leave handler receives a dangling pointer and crashes in +// QWaylandScreen::fromWlOutput / QPlatformScreen::screen(). +// +// Run with unpatched Qt 6.10.2: +// quickshell -p ./shell.qml +// +// Then toggle an output: +// niri msg action power-off-monitors (move mouse to wake) +// wlr-randr --output eDP-1 --off && sleep 0.3 && wlr-randr --output eDP-1 --on +// or unplug/replug an external monitor +// +// Should crash within a few seconds of the output toggle. + +import Quickshell +import Quickshell.Wayland + +ShellRoot { + // 20 layer-shell surfaces per screen - all bound to the output, + // so the compositor must send enter/leave for each on removal. + Variants { + model: Quickshell.screens + + Scope { + required property var modelData + + Repeater { + model: 20 + + PanelWindow { + screen: modelData + anchors { + top: true + left: true + } + width: 60 + height: 20 + margins.top: index * 22 + margins.left: 4 + exclusiveZone: 0 + color: "#80ff00ff" + } + } + } + } +} diff --git a/test/screen-uaf-reproducer/trigger.sh b/test/screen-uaf-reproducer/trigger.sh index 31609ff..3137cf8 100755 --- a/test/screen-uaf-reproducer/trigger.sh +++ b/test/screen-uaf-reproducer/trigger.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash # Trigger rapid output power-cycling to provoke the Qt Wayland screen UAF. -# Run the reproducer binary first, then this script in another terminal. +# +# First, in another terminal: +# nix run .#screen-uaf-reproducer-unpatched (should crash) +# nix run .#screen-uaf-reproducer-patched (should survive) +# +# Then run this script to toggle the output. # # Usage: ./trigger.sh [iterations] # iterations: number of off/on cycles (default 20)