Compare commits

..
5 changed files with 10 additions and 165 deletions

View file

@ -55,9 +55,8 @@
nixpkgs.lib.genAttrs systems (
system:
fn rec {
rawPkgs = nixpkgs.legacyPackages.${system};
pkgs = rawPkgs.extend qtbaseOverlay;
treefmt-eval = treefmt-nix.lib.evalModule rawPkgs treefmt-config;
pkgs = (nixpkgs.legacyPackages.${system}).extend qtbaseOverlay;
treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config;
}
);
in
@ -65,7 +64,7 @@
formatter = forAllSystems ({ treefmt-eval, ... }: treefmt-eval.config.build.wrapper);
packages = forAllSystems (
{ pkgs, rawPkgs, ... }:
{ pkgs, ... }:
let
# Rebuild quickshell against patched Qt via its overlay
qs = (pkgs.extend quickshell.overlays.default).quickshell.override {
@ -88,37 +87,14 @@
'';
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 ];
};
}
);
devShells = forAllSystems (
{ rawPkgs, ... }:
{ pkgs, ... }:
{
default = rawPkgs.mkShell {
packages = with rawPkgs; [
default = pkgs.mkShell {
packages = with pkgs; [
cargo
rustc
rust-analyzer
@ -132,25 +108,21 @@
);
checks = forAllSystems (
{
pkgs,
rawPkgs,
treefmt-eval,
}:
{ pkgs, treefmt-eval }:
{
formatting = treefmt-eval.config.build.check self;
build = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
nova-stats = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-stats;
docs = self.packages.${pkgs.stdenv.hostPlatform.system}.docs;
qmllint =
rawPkgs.runCommand "nova-shell-qmllint"
pkgs.runCommand "nova-shell-qmllint"
{
nativeBuildInputs = [ rawPkgs.qt6.qtdeclarative ];
nativeBuildInputs = [ pkgs.qt6.qtdeclarative ];
src = self;
}
''
cd $src
export QML_IMPORT_PATH="${rawPkgs.qt6.qtdeclarative}/lib/qt-6/qml:${
export QML_IMPORT_PATH="${pkgs.qt6.qtdeclarative}/lib/qt-6/qml:${
quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default
}/lib/qt-6/qml"
qmllint -E \

View file

@ -1,9 +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)

View file

@ -1,63 +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.
//
// 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 rapidly, 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
//
// The crash typically occurs within seconds of the output toggle.
// Creating many surfaces increases the odds that a surface.leave event
// lands in the same batch as the global_remove.
#include <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <QTimer>
#include <vector>
#include <cstdio>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// Many surfaces = higher chance of batched enter/leave events
constexpr int kWindows = 20;
std::vector<std::unique_ptr<QWindow>> windows;
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(1, 1);
w->setFlag(Qt::ToolTip); // small, no decoration, no focus
w->show();
windows.push_back(std::move(w));
}
std::fprintf(stderr,
"screen-uaf-reproducer: %d surfaces created on %d screen(s).\n"
"Toggle an output off/on to trigger the crash.\n",
kWindows, QGuiApplication::screens().size());
// Log screen changes so we know when the toggle happens
QObject::connect(&app, &QGuiApplication::screenAdded, [](QScreen *s) {
std::fprintf(stderr, " screenAdded: %s\n", qPrintable(s->name()));
});
QObject::connect(&app, &QGuiApplication::screenRemoved, [](QScreen *s) {
std::fprintf(stderr, " screenRemoved: %s\n", qPrintable(s->name()));
});
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

@ -1,44 +0,0 @@
#!/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.
#
# Usage: ./trigger.sh [iterations]
# iterations: number of off/on cycles (default 20)
set -euo pipefail
cycles="${1:-20}"
if command -v niri &>/dev/null && niri msg version &>/dev/null; then
echo "Detected Niri - using niri msg"
for i in $(seq 1 "$cycles"); do
echo "cycle $i/$cycles"
niri msg action power-off-monitors
sleep 0.3
niri msg action power-on-monitors
sleep 0.5
done
elif command -v wlr-randr &>/dev/null; then
output=$(wlr-randr --json 2>/dev/null | python3 -c \
"import sys,json; print(next(o['name'] for o in json.load(sys.stdin) if o['enabled']))" 2>/dev/null \
|| wlr-randr | grep -oP '^\S+' | head -1)
if [ -z "$output" ]; then
echo "error: could not detect an output via wlr-randr" >&2
exit 1
fi
echo "Detected wlroots compositor - toggling output $output"
for i in $(seq 1 "$cycles"); do
echo "cycle $i/$cycles"
wlr-randr --output "$output" --off
sleep 0.3
wlr-randr --output "$output" --on
sleep 0.5
done
else
echo "error: neither niri nor wlr-randr found" >&2
echo "Manually unplug/replug a monitor while the reproducer is running." >&2
exit 1
fi
echo "Done. If the reproducer is still alive, the bug did not trigger."
echo "Try increasing iterations or adding more surfaces."