rewrite reproducer as quickshell qml config with layer-shell surfaces
This commit is contained in:
parent
6fedbd72aa
commit
f00308b1f0
6 changed files with 72 additions and 125 deletions
37
flake.nix
37
flake.nix
|
|
@ -89,28 +89,21 @@
|
||||||
docs = pkgs.callPackage ./nix/docs.nix { inherit self; };
|
docs = pkgs.callPackage ./nix/docs.nix { inherit self; };
|
||||||
default = nova-shell;
|
default = nova-shell;
|
||||||
|
|
||||||
# Reproducer for the qtbase wayland screen UAF, built twice:
|
# Reproducer wrappers: run test/screen-uaf-reproducer/shell.qml
|
||||||
# patched (should survive) and unpatched (should crash).
|
# with patched vs unpatched quickshell to confirm the fix.
|
||||||
screen-uaf-reproducer-patched = pkgs.stdenv.mkDerivation {
|
screen-uaf-reproducer-patched = pkgs.writeShellScriptBin "screen-uaf-reproducer-patched" ''
|
||||||
pname = "screen-uaf-reproducer-patched";
|
exec ${qs}/bin/quickshell -p ${./test/screen-uaf-reproducer/shell.qml}
|
||||||
version = "0";
|
'';
|
||||||
src = ./test/screen-uaf-reproducer;
|
screen-uaf-reproducer-unpatched =
|
||||||
nativeBuildInputs = [
|
let
|
||||||
pkgs.cmake
|
qsUnpatched = (rawPkgs.extend quickshell.overlays.default).quickshell.override {
|
||||||
pkgs.qt6.wrapQtAppsHook
|
withX11 = false;
|
||||||
];
|
withI3 = false;
|
||||||
buildInputs = [ pkgs.qt6.qtbase ];
|
};
|
||||||
};
|
in
|
||||||
screen-uaf-reproducer-unpatched = rawPkgs.stdenv.mkDerivation {
|
rawPkgs.writeShellScriptBin "screen-uaf-reproducer-unpatched" ''
|
||||||
pname = "screen-uaf-reproducer-unpatched";
|
exec ${qsUnpatched}/bin/quickshell -p ${./test/screen-uaf-reproducer/shell.qml}
|
||||||
version = "0";
|
'';
|
||||||
src = ./test/screen-uaf-reproducer;
|
|
||||||
nativeBuildInputs = [
|
|
||||||
rawPkgs.cmake
|
|
||||||
rawPkgs.qt6.wrapQtAppsHook
|
|
||||||
];
|
|
||||||
buildInputs = [ rawPkgs.qt6.qtbase ];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
pkgs ? import <nixpkgs> { },
|
|
||||||
}:
|
|
||||||
pkgs.mkShell {
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
cmake
|
|
||||||
qt6.qtbase
|
|
||||||
qt6.wrapQtAppsHook
|
|
||||||
wlr-randr
|
|
||||||
];
|
|
||||||
}
|
|
||||||
51
test/screen-uaf-reproducer/shell.qml
Normal file
51
test/screen-uaf-reproducer/shell.qml
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Trigger rapid output power-cycling to provoke the Qt Wayland screen UAF.
|
# 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]
|
# Usage: ./trigger.sh [iterations]
|
||||||
# iterations: number of off/on cycles (default 20)
|
# iterations: number of off/on cycles (default 20)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue