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

commit8efc395508d769c4060e244e0c2f8bd27cbe14ac
parent50ee8a6ccc
authorLucas Galante <[email protected]>
date2026-08-17 15:54
fix: remove destroyed toplevel resources from the tracking list (UAF)

handle_destroy_toplevel_resource freed the per-resource data but left
the resource pointer in CceWindowManagement.toplevels. Latent while
nothing iterated that list; send_grid_patch does, so restarting the
grid client crashed the session: the dead instance's entry lingered and
the new instance's first patch send dereferenced the freed resource.
Shadow-verified against the exact repro (kill + respawn cce-grid, both
instances patch and latch, compositor survives).

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/cce_window_management.rs | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/server/cce_window_management.rs b/src/server/cce_window_management.rs
index f81e1e7..fbab96f 100644
--- a/src/server/cce_window_management.rs
+++ b/src/server/cce_window_management.rs
@@ -143,6 +143,18 @@ pub unsafe extern "C" fn cce_wm_get_cce_toplevel(
 unsafe extern "C" fn handle_destroy_toplevel_resource(resource: *mut ffi::wl_resource) {
     let data_ptr = ffi::wl_resource_get_user_data(resource) as *mut CceToplevelData;
     if !data_ptr.is_null() {
+        // Drop the tracking entry BEFORE the resource dies:
+        // send_grid_patch iterates `toplevels`, and a stale pointer there
+        // is a use-after-free on the next patch send — this crashed the
+        // session the first time the grid client was RESTARTED (the old
+        // entry lingered, the new instance's first patch dereferenced it).
+        let server = (*data_ptr).server;
+        if !server.is_null() {
+            (*server)
+                .cce_window_management
+                .toplevels
+                .retain(|&r| r != resource);
+        }
         let _ = Box::from_raw(data_ptr);
     }
 }