wayland: Phase 19 — clean SIGTERM shutdown (smoke test 22/22 PASS, exit 0)
Three bugs in the shutdown path caused wmaker to exit with SIGABRT (134) or hang on SIGTERM when running the Wayland backend: 1. wl_display_destroy() SIGABRT — wlroots 0.15 double-remove bug wlroots registers internal listeners on the XWayland wl_client's destroy signal. Those listeners self-remove during client destruction. wlr_xwayland_server_destroy() then tries wl_list_remove() on the same already-nulled links, corrupting the heap → SIGABRT. Fix: skip wl_display_destroy() entirely; Exit() calls exit(0) immediately after wl_display_close() returns, so the kernel reclaims all resources. 2. wl_display_destroy_clients() hang wlroots' XWayland client destroy handler performs blocking cleanup that can hang when Xwayland is in mid-dispatch. Fix: skip wl_display_destroy_clients() for the same reason as (1). 3. Global listener leak — dangling wl_listener.link pointers All globally-registered wl_listeners (output frame, backend new_output, xdg_shell new_surface, decoration, xwayland new_surface, keyboard key/ modifiers, pointer motion/button/frame) were never removed before the owning wlroots objects were freed. This left link.prev/next dangling into freed signal structs, causing heap corruption in later allocations. Fix: WL_LISTENER_REMOVE() all 11 global listeners in wl_display_close() before any display teardown. Additional fixes included in this phase: - handle_toplevel_destroy: remove all 10 per-view listeners (was only 4), preventing dangling pointers after each client session. - handle_output_frame: guard on wl_state.allocator; call wlr_scene_output_send_frame_done() when no GPU allocator so frame callbacks still fire (fixes weston-simple-shm stall on headless output). - handle_backend_new_output: call wlr_output_schedule_done() after a failed commit so weston-info does not hang waiting for wl_output.done. - XWayland shutdown: SIGKILL Xwayland before display teardown to prevent waitpid() hanging; null server->client to skip double-free in wlr_xwayland_server_destroy(). - Self-pipe shutdown: write() to a pipe from the SIGTERM signal handler; event loop reads it and calls wl_display_terminate() from loop context, avoiding async-signal-safety issues of calling display APIs from a handler. Smoke test result: 22/22 checks PASS, wmaker exits cleanly (status 0). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f86d294223
commit
3fd9926ba3
|
|
@ -82,6 +82,10 @@
|
|||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/backend.h>
|
||||
|
|
@ -217,6 +221,10 @@ struct wl_compositor_state {
|
|||
struct wl_listener output_frame_listener;
|
||||
char socket_name[64]; /* Phase 34: e.g. "wayland-0" */
|
||||
int started; /* non-zero after successful display_open */
|
||||
/* Self-pipe for async-signal-safe shutdown: signal handler writes to [1],
|
||||
* event loop reads [0] and calls wl_display_terminate(). */
|
||||
int shutdown_pipe[2];
|
||||
struct wl_event_source *shutdown_source;
|
||||
};
|
||||
|
||||
static struct wl_compositor_state wl_state;
|
||||
|
|
@ -228,6 +236,22 @@ static struct wl_compositor_state wl_state;
|
|||
/*
|
||||
* Phase 37: Wayland event loop timer — drives client frame callbacks.
|
||||
*/
|
||||
/* Called from within the event loop when the shutdown self-pipe becomes
|
||||
* readable. Calling wl_display_terminate() here is safe because we are
|
||||
* inside wl_event_loop_dispatch(), not in a signal handler. */
|
||||
static int
|
||||
wl_shutdown_pipe_handler(int fd, uint32_t mask, void *data)
|
||||
{
|
||||
char buf[16];
|
||||
(void)mask; (void)data;
|
||||
/* drain the pipe so the fd does not stay readable */
|
||||
while (read(fd, buf, sizeof(buf)) > 0)
|
||||
;
|
||||
if (wl_state.display)
|
||||
wl_display_terminate(wl_state.display);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
frame_timer_cb(void *data)
|
||||
{
|
||||
|
|
@ -497,10 +521,20 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data)
|
|||
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener);
|
||||
(void)data;
|
||||
|
||||
/* Remove all listeners registered with xdg_surface / xdg_toplevel signals.
|
||||
* Failure to remove a listener before freeing the view leaves a dangling
|
||||
* pointer in wlroots' signal list; when wlroots later emits or cleans up
|
||||
* those signals (e.g. during wl_display_destroy) it corrupts the heap. */
|
||||
wl_list_remove(&view->map_listener.link);
|
||||
wl_list_remove(&view->unmap_listener.link);
|
||||
wl_list_remove(&view->destroy_listener.link);
|
||||
wl_list_remove(&view->title_listener.link);
|
||||
wl_list_remove(&view->request_move_listener.link);
|
||||
wl_list_remove(&view->request_resize_listener.link);
|
||||
wl_list_remove(&view->request_maximize_listener.link);
|
||||
wl_list_remove(&view->request_fullscreen_listener.link);
|
||||
wl_list_remove(&view->request_minimize_listener.link);
|
||||
wl_list_remove(&view->set_app_id_listener.link);
|
||||
wl_list_remove(&view->link);
|
||||
/* Phase 18: remove scene node. */
|
||||
if (view->scene_node) {
|
||||
|
|
@ -828,8 +862,21 @@ static void
|
|||
handle_output_frame(struct wl_listener *listener, void *data)
|
||||
{
|
||||
(void)listener; (void)data;
|
||||
if (wl_state.scene_output)
|
||||
if (!wl_state.scene_output)
|
||||
return;
|
||||
if (wl_state.allocator) {
|
||||
/* Full scene commit — renders client surfaces and fires
|
||||
* wl_surface.frame callbacks so clients can submit next buffer. */
|
||||
wlr_scene_output_commit(wl_state.scene_output);
|
||||
} else {
|
||||
/* No GPU allocator available (e.g. headless without DRI access).
|
||||
* Skip rendering but still fire wl_surface.frame callbacks so
|
||||
* clients are not stalled waiting for frame pacing. Without
|
||||
* this, weston-simple-shm would never submit its second buffer. */
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
wlr_scene_output_send_frame_done(wl_state.scene_output, &now);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -859,8 +906,13 @@ handle_backend_new_output(struct wl_listener *listener, void *data)
|
|||
if (mode) wlr_output_set_mode(output, mode);
|
||||
}
|
||||
wlr_output_enable(output, true);
|
||||
if (!wlr_output_commit(output))
|
||||
if (!wlr_output_commit(output)) {
|
||||
wwarning("wl_backend: initial output commit failed");
|
||||
/* Send wl_output.done so Wayland clients (e.g. weston-info) do
|
||||
* not wait indefinitely for output configuration to be finalised.
|
||||
* On a failed commit wlroots does not send done automatically. */
|
||||
wlr_output_schedule_done(output);
|
||||
}
|
||||
|
||||
/* Register with the output layout. */
|
||||
if (wl_state.output_layout)
|
||||
|
|
@ -1169,6 +1221,26 @@ wl_display_open(const char *name)
|
|||
wwarning("wl_backend: virtual pointer initialised at %d,%d",
|
||||
wl_state.pointer_x, wl_state.pointer_y);
|
||||
|
||||
/* Self-pipe shutdown: signal handler writes a byte; the event loop reads
|
||||
* it here and calls wl_display_terminate() from within loop context,
|
||||
* avoiding the async-signal-safety issues of calling wl_display_destroy()
|
||||
* directly from a signal handler. */
|
||||
wl_state.shutdown_pipe[0] = wl_state.shutdown_pipe[1] = -1;
|
||||
wl_state.shutdown_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
|
||||
* instead of blocking after all bytes are consumed. */
|
||||
fcntl(wl_state.shutdown_pipe[0], F_SETFL, O_NONBLOCK);
|
||||
wl_state.shutdown_source = wl_event_loop_add_fd(loop,
|
||||
wl_state.shutdown_pipe[0],
|
||||
WL_EVENT_READABLE,
|
||||
wl_shutdown_pipe_handler,
|
||||
NULL);
|
||||
} else {
|
||||
wwarning("wl_backend: shutdown pipe creation failed; SIGTERM may crash");
|
||||
}
|
||||
|
||||
wl_state.started = 1;
|
||||
return NULL; /* no X11 Display* in the Wayland path */
|
||||
}
|
||||
|
|
@ -1178,39 +1250,117 @@ wl_display_close(void)
|
|||
{
|
||||
if (!wl_state.started)
|
||||
return;
|
||||
/* Phase 17: shut down XWayland before destroying the display. */
|
||||
/* Phase 17: shut down XWayland.
|
||||
*
|
||||
* wlroots 0.15 bug: wlr_xwayland_server_destroy() calls wl_client_destroy()
|
||||
* on the XWayland Wayland client. After any Wayland client has connected
|
||||
* and disconnected during the session, the XWayland server's wl_client ends
|
||||
* up in an inconsistent state: the client struct is freed but server->client
|
||||
* is NOT nulled. wlr_xwayland_server_destroy() then calls wl_client_destroy()
|
||||
* on the freed struct and crashes in wl_list_remove().
|
||||
*
|
||||
* Workaround: skip wlr_xwayland_destroy() entirely. The Xwayland process is
|
||||
* a child of wmaker and will receive SIGHUP/SIGKILL when the parent exits.
|
||||
* The X11 socket is in a tmpdir removed on exit. wl_display_destroy() (called
|
||||
* below) handles all remaining Wayland client/resource cleanup.
|
||||
*/
|
||||
if (wl_state.xwayland) {
|
||||
wlr_xwayland_destroy(wl_state.xwayland);
|
||||
/* wlroots 0.15 bug: wlr_xwayland_server_destroy() (called from the
|
||||
* display destroy signal below) sends SIGTERM to Xwayland and calls
|
||||
* waitpid(). If Xwayland is blocked waiting for compositor events,
|
||||
* waitpid() hangs indefinitely.
|
||||
*
|
||||
* Fix: pre-kill Xwayland with SIGKILL. SIGKILL cannot be caught or
|
||||
* deferred, so the process dies immediately (kernel-synchronous).
|
||||
* The zombie remains until wlroots' waitpid() reaps it — which now
|
||||
* returns instantly. The Wayland client struct (server->client) is
|
||||
* still valid because the event loop is stopped and the socket closure
|
||||
* has not been dispatched, so wlroots' wl_client_destroy() succeeds. */
|
||||
if (wl_state.xwayland->server) {
|
||||
if (wl_state.xwayland->server->pid > 0)
|
||||
kill(wl_state.xwayland->server->pid, SIGKILL);
|
||||
/* wlroots 0.15 bug: after any Wayland client has connected and
|
||||
* disconnected during the session, server->client holds a freed
|
||||
* (stale) wl_client pointer. wlr_xwayland_server_destroy() (fired
|
||||
* from the display destroy signal below) calls wl_client_destroy()
|
||||
* on this freed pointer, which corrupts the list and triggers
|
||||
* SIGABRT. Null the pointer so wlroots skips the destroy call.
|
||||
* If the client is still valid, wl_display_destroy_clients() will
|
||||
* clean it up; if it is stale, it has already been removed from
|
||||
* the display's client list and nothing more is needed. */
|
||||
wl_state.xwayland->server->client = NULL;
|
||||
}
|
||||
wl_state.xwayland = NULL;
|
||||
unsetenv("DISPLAY");
|
||||
}
|
||||
if (wl_state.frame_timer) {
|
||||
wl_event_source_remove(wl_state.frame_timer);
|
||||
wl_state.frame_timer = NULL;
|
||||
/* 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) {
|
||||
wl_event_source_remove(wl_state.shutdown_source);
|
||||
wl_state.shutdown_source = NULL;
|
||||
}
|
||||
if (wl_state.output_layout) {
|
||||
wlr_output_layout_destroy(wl_state.output_layout);
|
||||
wl_state.output_layout = NULL;
|
||||
if (wl_state.shutdown_pipe[0] >= 0) {
|
||||
close(wl_state.shutdown_pipe[0]);
|
||||
wl_state.shutdown_pipe[0] = -1;
|
||||
}
|
||||
/* Phase 18: destroy scene and allocator before backend. */
|
||||
if (wl_state.scene_output) {
|
||||
wlr_scene_output_destroy(wl_state.scene_output);
|
||||
wl_state.scene_output = NULL;
|
||||
if (wl_state.shutdown_pipe[1] >= 0) {
|
||||
close(wl_state.shutdown_pipe[1]);
|
||||
wl_state.shutdown_pipe[1] = -1;
|
||||
}
|
||||
if (wl_state.scene) {
|
||||
wlr_scene_node_destroy(&wl_state.scene->node);
|
||||
wl_state.scene = NULL;
|
||||
}
|
||||
if (wl_state.allocator) {
|
||||
wlr_allocator_destroy(wl_state.allocator);
|
||||
wl_state.allocator = NULL;
|
||||
}
|
||||
if (wl_state.renderer)
|
||||
wlr_renderer_destroy(wl_state.renderer);
|
||||
if (wl_state.backend)
|
||||
wlr_backend_destroy(wl_state.backend);
|
||||
if (wl_state.display)
|
||||
wl_display_destroy(wl_state.display);
|
||||
/* Remove all globally-registered wl_listener nodes before calling
|
||||
* wl_display_destroy(). wl_display_destroy() frees wlroots objects
|
||||
* (xdg_shell, backend, keyboard, pointer, output, etc.), which frees
|
||||
* their signal structs. Any wl_listener.link that is still in one of
|
||||
* those signal lists after the struct is freed becomes a dangling pointer.
|
||||
* Subsequent wl_list operations (in wayland-server cleanup or by glibc's
|
||||
* heap allocator detecting corruption) then trigger SIGABRT.
|
||||
* Guard each removal with a link.next check: wayland-server zeroes the
|
||||
* link after wl_list_remove(), so a non-NULL link means the listener is
|
||||
* currently inserted in a signal list. */
|
||||
#define WL_LISTENER_REMOVE(l) \
|
||||
do { if ((l).link.next) wl_list_remove(&(l).link); } while (0)
|
||||
|
||||
/* Output frame and new-output listeners */
|
||||
WL_LISTENER_REMOVE(wl_state.output_frame_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.backend_new_output_listener);
|
||||
/* XDG shell and decoration listeners */
|
||||
WL_LISTENER_REMOVE(wl_state.xdg_new_surface_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.decoration_new_listener);
|
||||
/* XWayland new-surface listener (xwayland ptr already nulled in step 1,
|
||||
* but the listener link is still inserted in the (now-orphaned) xwayland
|
||||
* object's signal list — remove it to prevent heap corruption). */
|
||||
WL_LISTENER_REMOVE(wl_state.xwayland_new_surface_listener);
|
||||
/* Keyboard listeners */
|
||||
WL_LISTENER_REMOVE(wl_state.keyboard_key_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.keyboard_modifiers_listener);
|
||||
/* Pointer listeners */
|
||||
WL_LISTENER_REMOVE(wl_state.pointer_motion_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.pointer_motion_abs_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.pointer_button_listener);
|
||||
WL_LISTENER_REMOVE(wl_state.pointer_frame_listener);
|
||||
#undef WL_LISTENER_REMOVE
|
||||
|
||||
/* Skip wl_display_destroy() and wl_display_destroy_clients().
|
||||
*
|
||||
* wl_display_destroy() fires the display's destroy signal which triggers
|
||||
* wlroots 0.15 internal object teardown. That teardown has a double-
|
||||
* remove bug in the XWayland path: wlroots registers a listener on the
|
||||
* Xwayland wl_client's destroy signal; that listener self-removes during
|
||||
* client destruction, and wlr_xwayland_server_destroy() then tries to
|
||||
* wl_list_remove() the already-nulled link, corrupting the heap
|
||||
* (SIGABRT).
|
||||
*
|
||||
* wl_display_destroy_clients() also blocks: wlroots' XWayland client
|
||||
* destroy handler performs blocking cleanup that can hang if Xwayland
|
||||
* is in the middle of protocol dispatch.
|
||||
*
|
||||
* Both calls are unnecessary here because Exit() calls exit(0) immediately
|
||||
* after wl_display_close() returns. The kernel reclaims all resources:
|
||||
* - file descriptors closed → all Wayland clients get POLLHUP
|
||||
* - memory freed by OS
|
||||
* - Xwayland already SIGKILL'd in step 1
|
||||
* - event sources removed in step 2
|
||||
* - all our signal listeners removed in step 3 */
|
||||
unsetenv("WAYLAND_DISPLAY");
|
||||
wl_state.renderer = NULL;
|
||||
wl_state.backend = NULL;
|
||||
|
|
@ -1218,6 +1368,11 @@ wl_display_close(void)
|
|||
wl_state.wl_backend_inner = NULL;
|
||||
wl_state.display = NULL;
|
||||
wl_state.output = NULL;
|
||||
wl_state.frame_timer = NULL;
|
||||
wl_state.output_layout = NULL;
|
||||
wl_state.scene_output = NULL;
|
||||
wl_state.scene = NULL;
|
||||
wl_state.allocator = NULL;
|
||||
wl_state.started = 0;
|
||||
}
|
||||
|
||||
|
|
@ -1247,6 +1402,23 @@ wl_event_loop_run(void)
|
|||
wl_display_run(wl_state.display);
|
||||
}
|
||||
|
||||
static void
|
||||
wl_event_loop_terminate(void)
|
||||
{
|
||||
/* Write a byte to the self-pipe. write(2) is async-signal-safe, so this
|
||||
* can safely be called from a signal handler. The event loop will read
|
||||
* the pipe and call wl_display_terminate() from within loop context,
|
||||
* avoiding the double-free that results from calling wl_display_destroy()
|
||||
* while wl_event_loop_dispatch() is active. */
|
||||
if (wl_state.shutdown_pipe[1] >= 0) {
|
||||
char c = 'T';
|
||||
(void)write(wl_state.shutdown_pipe[1], &c, 1);
|
||||
} else if (wl_state.display) {
|
||||
/* fallback if pipe creation failed at startup */
|
||||
wl_display_terminate(wl_state.display);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Screen open/close */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -2342,6 +2514,7 @@ const WMBackend wl_backend = {
|
|||
.display_default_screen = wl_display_default_screen,
|
||||
.atom_intern = wl_atom_intern,
|
||||
.event_loop_run = wl_event_loop_run,
|
||||
.event_loop_terminate = wl_event_loop_terminate,
|
||||
|
||||
.screen_open = wl_screen_open,
|
||||
.screen_close = wl_screen_close,
|
||||
|
|
|
|||
Loading…
Reference in New Issue