git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitf1e4f6dfa5683ce66d2965c401066e278ce20d17
parente85a18abd1
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-26 16:44
fix(xwayland): drop an override-redirect record before freeing it

handle_set_override_redirect freed the record when a window stopped
being override-redirect but left it in wm.override_redirects, unlike
handle_destroy. The next frame's apply_x11_scale pass read the freed
record and segfaulted in x11_scale_for. It took the live session down
on 2026-09-26: restarting cce-xembed-tray handed Wine's tray icon back
to the root window, per XEmbed, and Wine remapped it as a managed
window -- an override-redirect flip.

verify/clients gains or-flip, which maps a window override-redirect,
clears the flag and maps it again; the unfixed compositor crashes on
its first cycle in a shadow, the fixed one survives any number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                                |  9 ++++
 src/server/xwayland_override_redirect.rs |  8 +++-
 verify/clients/Cargo.toml                |  4 ++
 verify/clients/src/bin/or_flip.rs        | 70 ++++++++++++++++++++++++++++++++
 4 files changed, 90 insertions(+), 1 deletion(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index c1b8bd84..460a9f5a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -272,6 +272,15 @@ own `target/`, invisible to ccebuild), built on demand by the drivers:
   and leaving. Needs `cce-shadow start --xwayland`; see
   `../cce-status-interface/CLAUDE.md` for the bridge it tests.
 
+- **`or-flip`** — an X11 window that maps override-redirect, then is
+  unmapped, has the flag cleared and maps again (`--cycles N`), so wlroots
+  emits `set_override_redirect` and the record changes kind. Until
+  2026-09-26 the override-redirect side freed its record without dropping it
+  from `wm.override_redirects`, and the next frame's `apply_x11_scale`
+  segfaulted the compositor: a Wine tray icon handed back to the root window
+  by the XEmbed bridge did exactly this and took the live session down. The
+  compositor surviving a run is the assertion.
+
 `./verify/popup-constrain-test` drives `popup-nest` to prove submenus are
 fitted to the real screen (`xdg_popup.rs::handle_reposition`): a tall
 submenu low in the window slides up to fit, and one near the right edge
diff --git a/src/server/xwayland_override_redirect.rs b/src/server/xwayland_override_redirect.rs
index 36922443..2ea89492 100644
--- a/src/server/xwayland_override_redirect.rs
+++ b/src/server/xwayland_override_redirect.rs
@@ -348,7 +348,13 @@ unsafe extern "C" fn handle_set_override_redirect(listener: *mut ffi::wl_listene
 
     let server = (*or).server;
 
-    // Destroy this OR instance
+    // Destroy this OR instance. Drop it from the list first, as
+    // `handle_destroy` does: the per-frame `apply_x11_scale` pass walks
+    // `override_redirects`, and a freed entry left there crashed the
+    // compositor on its next frame (2026-09-26, a Wine tray icon handed
+    // back by the XEmbed bridge and remapped as a managed window;
+    // `verify/clients` `or-flip` reproduces it).
+    (*server).wm.override_redirects.retain(|&p| p != or);
     wl_listener_remove_safe(&mut (*or).request_configure);
     wl_listener_remove_safe(&mut (*or).destroy);
     wl_listener_remove_safe(&mut (*or).associate);
diff --git a/verify/clients/Cargo.toml b/verify/clients/Cargo.toml
index 05bdcf40..6d4fffee 100644
--- a/verify/clients/Cargo.toml
+++ b/verify/clients/Cargo.toml
@@ -24,6 +24,10 @@ path = "src/bin/popup_nest.rs"
 name = "xembed-icon"
 path = "src/bin/xembed_icon.rs"
 
+[[bin]]
+name = "or-flip"
+path = "src/bin/or_flip.rs"
+
 [dependencies]
 wayland-client = "0.31"
 wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
diff --git a/verify/clients/src/bin/or_flip.rs b/verify/clients/src/bin/or_flip.rs
new file mode 100644
index 00000000..5c5124e2
--- /dev/null
+++ b/verify/clients/src/bin/or_flip.rs
@@ -0,0 +1,70 @@
+// or-flip — an X11 window that stops being override-redirect between maps.
+//
+// Maps a window with override_redirect set, unmaps it, clears the flag and
+// maps it again, then keeps it up for a while. The second MapNotify carries
+// the changed flag, so wlroots emits `set_override_redirect` and the
+// compositor turns its override-redirect record into a managed window. An
+// XEmbed tray handing an icon back to the root window makes Wine do exactly
+// this, which is how the compositor crashed on 2026-09-26: the record was
+// freed but left in `wm.override_redirects`, and the next frame read it.
+//
+// Prints one line per step. Args: --hold SECS (default 3), --cycles N
+// (default 1; each cycle flips OR on and off again).
+
+use std::time::Duration;
+
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+use x11rb::wrapper::ConnectionExt as _;
+
+fn main() {
+    let mut hold = 3.0f64;
+    let mut cycles = 1u32;
+    let mut args = std::env::args().skip(1);
+    while let Some(a) = args.next() {
+        match a.as_str() {
+            "--hold" => hold = args.next().unwrap().parse().unwrap(),
+            "--cycles" => cycles = args.next().unwrap().parse().unwrap(),
+            other => panic!("unknown arg {other}"),
+        }
+    }
+    let (conn, screen_num) = x11rb::connect(None).expect("X connection");
+    let screen = conn.setup().roots[screen_num].clone();
+    let win = conn.generate_id().unwrap();
+    conn.create_window(
+        screen.root_depth,
+        win,
+        screen.root,
+        50,
+        50,
+        120,
+        90,
+        0,
+        WindowClass::INPUT_OUTPUT,
+        screen.root_visual,
+        &CreateWindowAux::new().background_pixel(screen.white_pixel).override_redirect(1),
+    )
+    .unwrap();
+    conn.change_property8(PropMode::REPLACE, win, AtomEnum::WM_NAME, AtomEnum::STRING, b"or-flip").unwrap();
+    let pause = |s: f64| std::thread::sleep(Duration::from_secs_f64(s));
+    for cycle in 0..cycles {
+        conn.change_window_attributes(win, &ChangeWindowAttributesAux::new().override_redirect(1)).unwrap();
+        conn.map_window(win).unwrap();
+        conn.flush().unwrap();
+        println!("cycle {cycle}: mapped override-redirect");
+        pause(0.5);
+        conn.unmap_window(win).unwrap();
+        conn.change_window_attributes(win, &ChangeWindowAttributesAux::new().override_redirect(0)).unwrap();
+        conn.map_window(win).unwrap();
+        conn.flush().unwrap();
+        println!("cycle {cycle}: remapped managed");
+        pause(0.5);
+        conn.unmap_window(win).unwrap();
+        conn.flush().unwrap();
+        pause(0.2);
+    }
+    conn.map_window(win).unwrap();
+    conn.flush().unwrap();
+    pause(hold);
+    println!("done");
+}