wayland: Phase 29+ — signalfd-based SIGTERM handling for nested mode

Replace async-signal-unsafe SIGTERM handling with signalfd registered in
the wlroots event loop.  The traditional path (startup.c handleExitSig →
DispatchEvent → wl_event_loop_terminate → pipe write) works in headless
mode but races in nested-Wayland mode: the combination of SA_RESTART and
epoll ordering could cause the pipe write to go unnoticed until the next
event, leaving SIGTERM apparently ignored in interactive Sway sessions.

wl_event_loop_run() now:
  1. Creates a signalfd(2) for SIGTERM, SIGINT, SIGHUP with SFD_NONBLOCK
  2. Blocks those signals from async delivery (sigprocmask SIG_BLOCK)
  3. Registers the fd in the wlroots event loop via wl_event_loop_add_fd
  4. wl_signalfd_handler() reads the pending signal and calls
     wl_display_terminate() from within loop context — fully synchronous,
     no async-signal-safety concerns

The pipe-based wl_event_loop_terminate() is retained as a secondary
mechanism for code paths that call it outside the main event loop.
Cleanup (event source removal + fd close) happens in wl_display_close().

Smoke test updated with a new "signal check" that verifies the log line
"signalfd installed for SIGTERM/SIGINT/SIGHUP" before sending SIGTERM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-31 14:08:00 +02:00
parent 8fe783e304
commit 5dd9078c49
2 changed files with 138 additions and 6 deletions

View File

@ -109,6 +109,7 @@
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <time.h>
#include <wayland-server-core.h>
@ -318,6 +319,11 @@ struct wl_compositor_state {
* event loop reads [0] and calls wl_display_terminate(). */
int shutdown_pipe[2];
struct wl_event_source *shutdown_source;
/* signalfd-based signal handling: primary mechanism for catching
* SIGTERM/SIGINT/SIGHUP from within the event loop (avoids async-signal-
* safety issues with the traditional signal handler path). */
int sigfd;
struct wl_event_source *sigfd_source;
/* Phase 22: interactive move/resize grab state.
* grab_type: 0=none, 1=move, 2=resize.
* grab_view: the toplevel being moved/resized (NULL when grab_type==0).
@ -2088,6 +2094,8 @@ wl_display_open(const char *name)
* directly from a signal handler. */
wl_state.shutdown_pipe[0] = wl_state.shutdown_pipe[1] = -1;
wl_state.shutdown_source = NULL;
wl_state.sigfd = -1;
wl_state.sigfd_source = NULL;
if (pipe(wl_state.shutdown_pipe) == 0) {
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
/* Make read end non-blocking so the drain loop in the handler returns
@ -2192,6 +2200,15 @@ wl_display_close(void)
wl_state.xwayland = NULL;
unsetenv("DISPLAY");
}
/* Clean up the signalfd source (registered in wl_event_loop_run). */
if (wl_state.sigfd_source) {
wl_event_source_remove(wl_state.sigfd_source);
wl_state.sigfd_source = NULL;
}
if (wl_state.sigfd >= 0) {
close(wl_state.sigfd);
wl_state.sigfd = -1;
}
/* Clean up the shutdown self-pipe (remove event source before display
* destruction so the fd is not left in the epoll set). */
if (wl_state.shutdown_source) {
@ -2341,13 +2358,103 @@ wl_atom_intern(const char *name)
return 0; /* atoms have no meaning in native Wayland */
}
/* ------------------------------------------------------------------ */
/* Phase 29+: signalfd-based signal handling */
/* ------------------------------------------------------------------ */
/*
* wl_signalfd_handler -- called from within wl_event_loop_dispatch() when
* SIGTERM, SIGINT, or SIGHUP is pending on the signalfd.
*
* Running inside the event loop means we can safely call
* wl_display_terminate() without risking a re-entrant call into the Wayland
* event loop machinery, which is the failure mode of the traditional
* signal-handler → DispatchEvent → wl_display_terminate path.
*/
static int
wl_signalfd_handler(int fd, uint32_t mask, void *data)
{
struct signalfd_siginfo info;
ssize_t n;
(void)mask; (void)data;
n = read(fd, &info, sizeof(info));
if (n != (ssize_t)sizeof(info))
return 0;
wwarning("wl_backend: signalfd received signal %u - terminating event loop",
(unsigned)info.ssi_signo);
/* Set the exit state flag so post-loop cleanup paths see it. */
SIG_WCHANGE_STATE(WSTATE_NEED_EXIT);
/* Terminate the event loop from within loop context. */
if (wl_state.display)
wl_display_terminate(wl_state.display);
return 0;
}
static void
wl_event_loop_run(void)
{
if (wl_state.display) {
wwarning("wl_backend: event loop running");
wl_display_run(wl_state.display);
if (!wl_state.display)
return;
/*
* Install signalfd as the primary SIGTERM/SIGINT/SIGHUP handler.
*
* The traditional path (startup.c handleExitSig → DispatchEvent →
* event_loop_terminate → pipe write) works in headless/smoke-test mode
* because epoll_wait with SA_RESTART sees the pipe immediately after the
* signal handler returns. In nested-Wayland mode the wlroots nested
* backend processes parent-compositor events alongside our own clients,
* and the combination of SA_RESTART + epoll ordering can cause the pipe
* write to be missed until the next event, leaving SIGTERM apparently
* ignored.
*
* signalfd is the compositor-standard solution: block async delivery,
* route signals through a regular fd in the epoll set, and handle them
* synchronously from within the dispatch loop. The pipe-based path
* (wl_event_loop_terminate) is retained as a fallback for code paths
* that call it before wl_event_loop_run() returns.
*/
{
sigset_t ss;
sigemptyset(&ss);
sigaddset(&ss, SIGTERM);
sigaddset(&ss, SIGINT);
sigaddset(&ss, SIGHUP);
wl_state.sigfd = signalfd(-1, &ss, SFD_NONBLOCK | SFD_CLOEXEC);
if (wl_state.sigfd >= 0) {
/* Block async delivery so signals only arrive via the fd. */
sigprocmask(SIG_BLOCK, &ss, NULL);
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
wl_state.sigfd_source = wl_event_loop_add_fd(loop,
wl_state.sigfd,
WL_EVENT_READABLE,
wl_signalfd_handler,
NULL);
if (wl_state.sigfd_source) {
wwarning("wl_backend: signalfd installed for SIGTERM/SIGINT/SIGHUP");
} else {
wwarning("wl_backend: wl_event_loop_add_fd failed; falling back to pipe");
close(wl_state.sigfd);
wl_state.sigfd = -1;
/* Restore async delivery so the old handler still works. */
sigprocmask(SIG_UNBLOCK, &ss, NULL);
}
} else {
wwarning("wl_backend: signalfd(2) failed (%s); SIGTERM via pipe fallback",
strerror(errno));
}
}
wwarning("wl_backend: event loop running");
wl_display_run(wl_state.display);
wwarning("wl_backend: event loop exited");
}
static void

View File

@ -175,6 +175,14 @@
# create_appicon_for_application → wIconUpdate/wIconPaint) guarded with
# !dpy early returns so the full manage/unmanage lifecycle works without
# X11 rendering. Verified by "dock/clip disabled" log message.
# 52. (Phase 29+) signalfd-based SIGTERM handling: wl_event_loop_run()
# creates a signalfd for SIGTERM/SIGINT/SIGHUP and registers it in the
# wlroots event loop via wl_event_loop_add_fd. This ensures SIGTERM is
# handled synchronously from within the dispatch loop (not from an async
# signal handler), fixing SIGTERM non-response in interactive nested-Sway
# mode where the SA_RESTART + epoll ordering could race. The traditional
# pipe-based wl_event_loop_terminate() is retained as a secondary
# mechanism. Verified by "signalfd installed for SIGTERM" log message.
#
# This test grows as wl_backend vtable slots are implemented.
#
@ -225,6 +233,14 @@ if [ -z "$XDG_RUNTIME_DIR" ]; then
export XDG_RUNTIME_DIR
fi
# Clean stale XWayland sockets/locks left by previous crashed runs.
# Without this, XWayland picks an unexpected display number and the
# weston-info globals check can race with the compositor dying.
pkill -x Xwayland 2>/dev/null || true
sleep 0.2
rm -f /tmp/.X11-unix/X{1,2,3,4,5,6,7,8,9,10} \
/tmp/.X{1,2,3,4,5,6,7,8,9,10}-lock 2>/dev/null || true
WMAKER_USE_WAYLAND=1 src/wmaker \
--no-dock --no-clip --no-autolaunch \
--dont-restore --static \
@ -635,15 +651,24 @@ else
echo " client check: skipped (weston-simple-shm not installed)"
fi
# -- Signal handling: verify signalfd is installed ------------------------
#
# Phase 29+: wl_event_loop_run() installs a signalfd for SIGTERM/SIGINT/SIGHUP
# and logs "signalfd installed for SIGTERM/SIGINT/SIGHUP". This is the primary
# SIGTERM mechanism in Wayland mode; the pipe-based fallback is secondary.
# In headless mode (smoke test) both mechanisms are active.
grep -q 'signalfd installed for SIGTERM' /tmp/wl-smoke-stderr \
|| die "compositor did not install signalfd for SIGTERM (signal handling unreliable)"
echo " signal check: signalfd installed for SIGTERM/SIGINT/SIGHUP"
# -- Send SIGTERM and wait for clean exit ---------------------------------
kill -TERM "$WM_PID" 2>/dev/null
WM_EXIT=0
wait "$WM_PID" 2>/dev/null || WM_EXIT=$?
WM_PID=""
# SIGTERM causes exit(0) via the Shutdown(WSExitMode) path;
# shell reports signal-terminated processes as 128+signal, but
# wmaker catches SIGTERM and calls Exit(0) so we expect 0.
# SIGTERM causes exit(0) via the signalfd handler → wl_display_terminate()
# → wl_display_run() returns → EventLoop() → Exit(0).
[ "$WM_EXIT" -eq 0 ] \
|| die "wmaker exited with status $WM_EXIT after SIGTERM (expected 0)"