From 13086cb8983f074df575560831745fd11a4f98a6 Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:06:33 +0100 Subject: [PATCH 1/9] quit when the backend is destroyed When the session goes away, e.g. seatd being stopped at shutdown, wlroots destroys the backend by itself (wlr_auto_backend_monitor), and wlr_backend_finish() aborts since dwl's new_output and new_input listeners are still attached: Assertion failed: wl_list_empty(&backend->events.new_input.listener_list) Detach them and quit instead. --- dwl.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 76bb628..ca05f99 100644 --- a/dwl.c +++ b/dwl.c @@ -298,6 +298,7 @@ static void createpopup(struct wl_listener *listener, void *data); static void cursorconstrain(struct wlr_pointer_constraint_v1 *constraint); static void cursorframe(struct wl_listener *listener, void *data); static void cursorwarptohint(void); +static void destroybackend(struct wl_listener *listener, void *data); static void destroydecoration(struct wl_listener *listener, void *data); static void destroydragicon(struct wl_listener *listener, void *data); static void destroyidleinhibitor(struct wl_listener *listener, void *data); @@ -455,6 +456,7 @@ static struct wl_array ghosts; /* struct wlr_scene_node *, unmapped clients */ static struct wl_array offmons; /* char *, names of outputs turned off by clients */ /* global event handlers */ +static struct wl_listener backend_destroy = {.notify = destroybackend}; static struct wl_listener cursor_axis = {.notify = axisnotify}; static struct wl_listener cursor_button = {.notify = buttonpress}; static struct wl_listener cursor_frame = {.notify = cursorframe}; @@ -849,7 +851,8 @@ cleanup(void) /* If it's not destroyed manually, it will cause a use-after-free of wlr_seat. * Destroy it until it's fixed on the wlroots side */ - wlr_backend_destroy(backend); + if (backend) + wlr_backend_destroy(backend); if (transaction_idle) wl_event_source_remove(transaction_idle); @@ -905,6 +908,7 @@ cleanuplisteners(void) wl_list_remove(&new_idle_inhibitor.link); wl_list_remove(&layout_change.link); wl_list_remove(&new_input_device.link); + wl_list_remove(&backend_destroy.link); wl_list_remove(&new_virtual_keyboard.link); wl_list_remove(&new_virtual_pointer.link); wl_list_remove(&new_pointer_constraint.link); @@ -1407,6 +1411,21 @@ cursorwarptohint(void) } } +void +destroybackend(struct wl_listener *listener, void *data) +{ + /* wlroots destroys the backend itself when the session goes away (e.g. + * seatd stopped at shutdown), and asserts that its listeners are gone */ + wl_list_remove(&new_output.link); + wl_list_init(&new_output.link); + wl_list_remove(&new_input_device.link); + wl_list_init(&new_input_device.link); + wl_list_remove(&backend_destroy.link); + wl_list_init(&backend_destroy.link); + backend = NULL; + quit(NULL); +} + void destroydecoration(struct wl_listener *listener, void *data) { @@ -2742,6 +2761,7 @@ setup(void) * if an X11 server is running. */ if (!(backend = wlr_backend_autocreate(event_loop, &session))) die("couldn't create backend"); + wl_signal_add(&backend->events.destroy, &backend_destroy); /* Initialize the scene graph used to lay out windows */ scene = wlr_scene_create(); From 1f783b71a393ceac7b7d2f1aa194848c736a846b Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:06:47 +0100 Subject: [PATCH 2/9] fix a buffer leak in outputmgrapplyortest() wlr_swapchain_acquire() returns a locked buffer and wlr_output_state_set_buffer() takes its own lock, so every output enabled through output management (wlr-randr, kanshi) leaked a buffer. Unlock it once the state holds it, as wlr_output_swapchain_manager does. --- dwl.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index ca05f99..6662022 100644 --- a/dwl.c +++ b/dwl.c @@ -2284,8 +2284,12 @@ outputmgrapplyortest(struct wlr_output_configuration_v1 *config, int test) for (i = 0; i < states_len; i++) { struct wlr_swapchain *swapchain = wlr_output_swapchain_manager_get_swapchain( &swapchain_manager, states[i].output); - if (swapchain && !states[i].output->enabled) - wlr_output_state_set_buffer(&states[i].base, wlr_swapchain_acquire(swapchain)); + struct wlr_buffer *buffer; + if (swapchain && !states[i].output->enabled + && (buffer = wlr_swapchain_acquire(swapchain))) { + wlr_output_state_set_buffer(&states[i].base, buffer); + wlr_buffer_unlock(buffer); + } } if (!(ok = wlr_backend_commit(backend, states, states_len))) From 0b053233c2d2de10103955b9fdea4c204582bcd3 Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:07:03 +0100 Subject: [PATCH 3/9] remove the popup commit listener when the popup is destroyed The listener was only removed after the popup's initial commit was handled. If the popup went away before that (closed before its first commit, no parent, or destroyed by commitpopup() because its client has no monitor), it stayed on the surface: wlroots then asserts when the surface is destroyed, and a commit in between dereferences a NULL popup. --- dwl.c | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/dwl.c b/dwl.c index 6662022..6dbcebb 100644 --- a/dwl.c +++ b/dwl.c @@ -246,6 +246,11 @@ typedef struct { struct wl_listener destroy; } PointerConstraint; +typedef struct { + struct wl_listener commit; + struct wl_listener destroy; +} Popup; + typedef struct { const char *id; const char *title; @@ -307,6 +312,7 @@ static void destroylock(SessionLock *lock, int unlocked); static void destroylocksurface(struct wl_listener *listener, void *data); static void destroynotify(struct wl_listener *listener, void *data); static void destroypointerconstraint(struct wl_listener *listener, void *data); +static void destroypopup(struct wl_listener *listener, void *data); static void destroysessionlock(struct wl_listener *listener, void *data); static void destroykeyboardgroup(struct wl_listener *listener, void *data); static Monitor *dirtomon(enum wlr_direction dir); @@ -1054,6 +1060,7 @@ commitpopup(struct wl_listener *listener, void *data) { struct wlr_surface *surface = data; struct wlr_xdg_popup *popup = wlr_xdg_popup_try_from_wlr_surface(surface); + Popup *p = wl_container_of(listener, p, commit); LayerSurface *l = NULL; Client *c = NULL; struct wlr_box box; @@ -1075,8 +1082,7 @@ commitpopup(struct wl_listener *listener, void *data) box.x -= (type == LayerShell ? l->scene->node.x : c->geom.x - c->surface.xdg->geometry.x); box.y -= (type == LayerShell ? l->scene->node.y : c->geom.y - c->surface.xdg->geometry.y); wlr_xdg_popup_unconstrain_from_box(popup, &box); - wl_list_remove(&listener->link); - free(listener); + destroypopup(&p->destroy, NULL); } void @@ -1370,7 +1376,9 @@ createpopup(struct wl_listener *listener, void *data) /* This event is raised when a client (either xdg-shell or layer-shell) * creates a new popup. */ struct wlr_xdg_popup *popup = data; - LISTEN_STATIC(&popup->base->surface->events.commit, commitpopup); + Popup *p = ecalloc(1, sizeof(*p)); + LISTEN(&popup->base->surface->events.commit, &p->commit, commitpopup); + LISTEN(&popup->events.destroy, &p->destroy, destroypopup); } void @@ -1559,6 +1567,15 @@ destroypointerconstraint(struct wl_listener *listener, void *data) free(pointer_constraint); } +void +destroypopup(struct wl_listener *listener, void *data) +{ + Popup *p = wl_container_of(listener, p, destroy); + wl_list_remove(&p->commit.link); + wl_list_remove(&p->destroy.link); + free(p); +} + void destroysessionlock(struct wl_listener *listener, void *data) { From 1f5b30aeb34ab259b225839f04806c9865a2fb84 Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:07:16 +0100 Subject: [PATCH 4/9] don't return a freed client for a surface whose toplevel is gone A client can destroy its xdg_toplevel and keep the xdg_surface. wlroots keeps the role but sets toplevel to NULL, and xdg_surface->data still points to the Client freed by destroynotify(), so e.g. a popup created on that surface made toplevel_from_wlr_surface() read freed memory. --- client.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client.h b/client.h index bea7a97..123a584 100644 --- a/client.h +++ b/client.h @@ -98,6 +98,8 @@ toplevel_from_wlr_surface(struct wlr_surface *s, Client **pc, LayerSurface **pl) xdg_surface = tmp_xdg_surface; break; case WLR_XDG_SURFACE_ROLE_TOPLEVEL: + if (!xdg_surface->toplevel) + return -1; c = xdg_surface->data; type = c->type; goto end; From 8fde769b788bee6462ad7d2b83bc0f78a136e2ed Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:07:16 +0100 Subject: [PATCH 5/9] don't give the keyboard back to a lock surface being destroyed wlroots still lists the lock surface being destroyed, and inserts new ones at the head, so when the focused surface was the newest one, destroylocksurface() gave the focus back to it and then nothing had it: the password could not be typed until an output changed. Happens e.g. when unplugging a monitor while locked with waylock. --- dwl.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dwl.c b/dwl.c index 6dbcebb..921f353 100644 --- a/dwl.c +++ b/dwl.c @@ -1512,14 +1512,21 @@ destroylocksurface(struct wl_listener *listener, void *data) if (lock_surface->surface != seat->keyboard_state.focused_surface) return; - if (locked && cur_lock && !wl_list_empty(&cur_lock->surfaces)) { - surface = wl_container_of(cur_lock->surfaces.next, surface, link); - client_notify_enter(surface->surface, wlr_seat_get_keyboard(seat)); - } else if (!locked) { + if (!locked) { focusclient(focustop(selmon), NULL, 1); - } else { - wlr_seat_keyboard_clear_focus(seat); + return; } + + /* The surface being destroyed is still in the list */ + if (cur_lock) { + wl_list_for_each(surface, &cur_lock->surfaces, link) { + if (surface != lock_surface) { + client_notify_enter(surface->surface, wlr_seat_get_keyboard(seat)); + return; + } + } + } + wlr_seat_keyboard_clear_focus(seat); } void From d1f58218b350d7719e4e9720ab5599c23e4b101d Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:07:32 +0100 Subject: [PATCH 6/9] fix a crash in focusmon() when no monitor is enabled closemon() sets selmon to NULL when every output is disabled (e.g. turned off with wlr-randr), and dirtomon() dereferences it. --- dwl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwl.c b/dwl.c index 921f353..259ebab 100644 --- a/dwl.c +++ b/dwl.c @@ -1702,6 +1702,8 @@ void focusmon(const Arg *arg) { int i = 0, nmons = wl_list_length(&mons); + if (!selmon) + return; if (nmons) { do /* don't switch to disabled mons */ selmon = dirtomon(arg->i); From 8df3f1f96f5e0c0fbccfc782e98152503f35ae8a Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:07:32 +0100 Subject: [PATCH 7/9] focus the dialogs of a fullscreen client Since a6e2c15 focustop() returns the fullscreen client even when one of its children is shown above it, so a dialog opened by a fullscreen client (a file chooser, say) was shown on top but didn't get focus. Let focustop() return the fullscreen client's children too, and keep fullscreen_bg on while one of them is focused. --- dwl.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/dwl.c b/dwl.c index 259ebab..ce1f746 100644 --- a/dwl.c +++ b/dwl.c @@ -1744,14 +1744,18 @@ focusstack(const Arg *arg) Client * focustop(Monitor *m) { - Client *c; + Client *c, *fs = NULL; - wl_list_for_each(c, &fstack, flink) - if (VISIBLEON(c, m) && c->isfullscreen) - return c; + wl_list_for_each(c, &fstack, flink) { + if (VISIBLEON(c, m) && c->isfullscreen) { + fs = c; + break; + } + } + /* A fullscreen client's children are shown above it */ wl_list_for_each(c, &fstack, flink) - if (VISIBLEON(c, m)) + if (VISIBLEON(c, m) && (!fs || c == fs || client_get_parent(c) == fs)) return c; return NULL; } @@ -3185,7 +3189,7 @@ void transactionapply(void) { struct wlr_scene_node **ghost; - Client *c; + Client *c, *p; Monitor *m; wl_event_source_timer_update(transaction_timer, 0); @@ -3207,7 +3211,8 @@ transactionapply(void) wl_list_for_each(m, &mons, link) { if (m->wlr_output->enabled) wlr_scene_node_set_enabled(&m->fullscreen_bg->node, - (c = focustop(m)) && c->isfullscreen); + (c = focustop(m)) && (c->isfullscreen + || ((p = client_get_parent(c)) && p->isfullscreen))); } motionnotify(0, NULL, 0, 0, 0, 0); From 568d047595d08beda5f486d43db8b240361d838d Mon Sep 17 00:00:00 2001 From: dvh Date: Wed, 11 Nov 2026 10:09:02 +0100 Subject: [PATCH 8/9] don't keep outputs unplugged while off in offmons When an output was unplugged while turned off (e.g. by swayidle), only the remaining ones were turned on again, and the unplugged one stayed in offmons, so it stayed off when plugged in again. Forget the outputs that are no longer connected whenever one is turned on. --- dwl.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dwl.c b/dwl.c index ce1f746..8db7fc7 100644 --- a/dwl.c +++ b/dwl.c @@ -320,6 +320,7 @@ static void focusclient(Client *c, LayerSurface *l, int lift); static void focusmon(const Arg *arg); static void focusstack(const Arg *arg); static Client *focustop(Monitor *m); +static void forgetoffmons(void); static void fullscreennotify(struct wl_listener *listener, void *data); static void gpureset(struct wl_listener *listener, void *data); static void handlesig(int signo); @@ -1760,6 +1761,26 @@ focustop(Monitor *m) return NULL; } +void +forgetoffmons(void) +{ + /* Forget the outputs that were unplugged while off, or they would stay + * off when plugged in again */ + char **name, **kept = offmons.data; + Monitor *m; + + wl_array_for_each(name, &offmons) { + wl_list_for_each(m, &mons, link) + if (!strcmp(*name, m->wlr_output->name)) + break; + if (&m->link != &mons) + *kept++ = *name; + else + free(*name); + } + offmons.size = (char *)kept - (char *)offmons.data; +} + void fullscreennotify(struct wl_listener *listener, void *data) { @@ -2451,6 +2472,8 @@ powermgrsetmode(struct wl_listener *listener, void *data) if (!m) return; + if (event->mode) + forgetoffmons(); m->gamma_lut_changed = 1; /* Reapply gamma LUT when re-enabling the output */ wlr_output_state_set_enabled(&state, event->mode); /* The output was reconnected while off and was never modeset */ From efbe686e63af2deb92df03f5fda34d35233ba706 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Wed, 11 Nov 2026 11:13:53 +0100 Subject: [PATCH 9/9] remove LISTEN_STATIC It's only used in createidleinhibitor(). --- dwl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 8db7fc7..06b5c07 100644 --- a/dwl.c +++ b/dwl.c @@ -83,7 +83,6 @@ #define VISIBLEON(C, M) ((M) && (C)->mon == (M) && ((C)->tags & (M)->tagset[(M)->seltags])) #define TAGMASK ((1u << TAGCOUNT) - 1) #define LISTEN(E, L, H) wl_signal_add((E), ((L)->notify = (H), (L))) -#define LISTEN_STATIC(E, H) do { struct wl_listener *_l = ecalloc(1, sizeof(*_l)); _l->notify = (H); wl_signal_add((E), _l); } while (0) /* enums */ enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */ @@ -1103,7 +1102,8 @@ void createidleinhibitor(struct wl_listener *listener, void *data) { struct wlr_idle_inhibitor_v1 *idle_inhibitor = data; - LISTEN_STATIC(&idle_inhibitor->events.destroy, destroyidleinhibitor); + struct wl_listener *destroy = ecalloc(1, sizeof(*destroy)); + LISTEN(&idle_inhibitor->events.destroy, destroy, destroyidleinhibitor); checkidleinhibitor(NULL); }