Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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");
+}