From 53a8d0bea2c1a361c1a0c57b20b4adc4b748c2a0 Mon Sep 17 00:00:00 2001 From: Timo Strunk Date: Wed, 22 Jul 2026 22:31:41 +0200 Subject: [PATCH] fix(kms): join surface threads before exit to avoid SIGABRT on shutdown When the kiosk child exits, calling process::exit() directly from the calloop callback runs libc's atexit handlers (Mesa's util_queue_kill_threads) while a KMS surface thread may still be mid-frame in eglCreateSync(). Concurrent Mesa teardown and live EGL use corrupt the heap and glibc aborts the process ("free(): corrupted unsorted chunks", SIGABRT). Stop the event loop cleanly via should_stop instead, stashing the child's exit code in Common::kiosk_exit_code. After the loop returns, pause() each DRM device (releasing master so the surface drop path skips its blocking clear_state(), and the incoming compositor gets master immediately), then drop_and_join() every surface so no thread is left in Mesa. Only then is process::exit() called, at which point atexit is safe to run. The join is done here rather than in Surface::Drop because an unconditional join in Drop deadlocks against apply_config_for_outputs during normal operation; once the event loop has stopped, the surface thread can process ThreadCommand::End and exit without the main thread servicing any pending message. https://github.com/pop-os/cosmic-comp/issues/2375 Authored by Claude Opus 4.8 Reviewed by Timo Strunk Co-Authored-By: Claude Opus 4.8 --- src/lib.rs | 36 ++++++++++++++++++++++++++++-------- src/state.rs | 2 ++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 542162eb..f7282942 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ use smithay::{ }; use anyhow::{Context, Result}; -use state::{LastRefresh, State}; +use state::{BackendData, LastRefresh, State}; use std::{ env, ffi::OsString, @@ -222,19 +222,17 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { // Kiosk child exited with status Ok(Some(exit_status)) => { info!("Command exited with status {:?}", exit_status); - match exit_status.code() { - // Exiting with the same status as the kiosk child - Some(code) => process::exit(code), - // The kiosk child exited with signal, exiting with error - None => process::exit(1), - } + // Stop cleanly so surface threads are joined before exit() (signal -> 1). + state.common.kiosk_exit_code = Some(exit_status.code().unwrap_or(1)); + state.common.should_stop = true; } // Command still running Ok(None) => {} // Kiosk child disappeared, exiting with error Err(err) => { warn!(?err, "Failed to wait for command"); - process::exit(1); + state.common.kiosk_exit_code = Some(1); + state.common.should_stop = true; } } } @@ -245,10 +243,32 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { let _ = child.kill(); } + let kiosk_exit_code = state.common.kiosk_exit_code; + + // Join surface threads before exit() so no thread is mid-eglCreateSync when + // Mesa's atexit handlers run and corrupt the heap (issue #2375). Safe here + // because the event loop has stopped; an unconditional join in Surface::Drop + // would instead deadlock against apply_config_for_outputs. + if let BackendData::Kms(kms) = &mut state.backend { + // Release master first so the surface drop path skips its blocking commit. + for device in kms.drm_devices.values_mut() { + device.drm.pause(); + } + for device in kms.drm_devices.values_mut() { + for (_, surface) in device.inner.surfaces.drain() { + surface.drop_and_join(); + } + } + } + // drop eventloop & state before logger std::mem::drop(event_loop); std::mem::drop(state); + if let Some(code) = kiosk_exit_code { + process::exit(code); + } + Ok(()) } diff --git a/src/state.rs b/src/state.rs index 21cbb400..ce546505 100644 --- a/src/state.rs +++ b/src/state.rs @@ -244,6 +244,7 @@ pub struct Common { pub clock: Clock, pub startup_done: Arc, pub should_stop: bool, + pub kiosk_exit_code: Option, pub gesture_state: Option, @@ -754,6 +755,7 @@ impl State { clock, startup_done: Arc::new(AtomicBool::new(false)), should_stop: false, + kiosk_exit_code: None, gesture_state: None, kiosk_child: None,