rewrite reproducer as quickshell qml config with layer-shell surfaces

This commit is contained in:
Damocles 2026-04-20 22:01:48 +02:00
parent 6fedbd72aa
commit f00308b1f0
6 changed files with 72 additions and 125 deletions

View file

@ -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)

View file

@ -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 <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <QTimer>
#include <vector>
#include <cstdio>
static std::vector<std::unique_ptr<QWindow>> 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<QWindow>();
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<int>(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();
}

View file

@ -1,11 +0,0 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
cmake
qt6.qtbase
qt6.wrapQtAppsHook
wlr-randr
];
}

View file

@ -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"
}
}
}
}
}

View file

@ -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)