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 <Timo.Strunk@gmail.com>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Timo Strunk 2026-07-22 22:31:41 +02:00 • committed by Jacob Kauffmann
parent 9718699ec7
commit 53a8d0bea2
2 changed files with 30 additions and 8 deletions

View file

@ -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<dyn Error>> {
// 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<dyn Error>> {
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(())
}

View file

@ -244,6 +244,7 @@ pub struct Common {
pub clock: Clock<Monotonic>,
pub startup_done: Arc<AtomicBool>,
pub should_stop: bool,
pub kiosk_exit_code: Option<i32>,
pub gesture_state: Option<GestureState>,
@ -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,