From c403a47894a1fa856ea1a913b30d350185a0b34b Mon Sep 17 00:00:00 2001 From: Han Boetes Date: Sun, 1 Mar 2026 21:58:03 +0100 Subject: [PATCH 1/6] Fix use-after-free crash in cursor surface handling ### Problem `setcursor()` stores the client-provided `wlr_surface` pointer in `last_cursor.surface`, but never registers a destroy listener on it. When the client exits (e.g. closing a launcher like fuzzel), the surface is destroyed, but `last_cursor.surface` still holds the stale pointer. If the cursor hide timeout fires while the cursor surface is alive, and the client then exits, the next mouse movement calls `handlecursoractivity()`, which passes the dangling pointer to `wlr_cursor_set_surface()`. This causes a SIGSEGV in `wl_list_insert()` inside libwayland-server, as the `wl_list` embedded in the destroyed surface struct has been freed. A secondary issue exists in `setcursorshape()`: when a client switches from a custom cursor surface to a shape cursor, `last_cursor.surface` is set to NULL but the destroy listener (if registered) is not removed, leaving a dangling listener on the destroyed surface. The crash only manifests when `cursor_hidden` is true at the moment of the mouse movement, which is why it is intermittent and difficult to reproduce. ### Root cause Confirmed via `coredumpctl debug` and `bt full`: ``` #0 wl_list_insert (libwayland-server.so) #1 wlr_cursor_set_surface (libwlroots) #2 handlecursoractivity (mango.c) #3 motionnotify (mango.c) #4 motionrelative (mango.c) #5 wl_signal_emit_mutable #6 handle_libinput_readable ``` ### Fix - Add a `wl_listener` (`last_cursor_surface_destroy_listener`) that clears `last_cursor.surface` and removes itself when the surface is destroyed. - Initialize the listener's link in `setup()` so `wl_list_empty()` checks are reliable from the start. - In `setcursor()`, remove any existing listener before registering a new one on the incoming surface. - In `setcursorshape()`, remove the destroy listener when switching to a shape cursor. - Add a NULL guard in `handlecursoractivity()` as a safety net. --- src/mango.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/mango.c b/src/mango.c index 0864a982..cb710d2e 100644 --- a/src/mango.c +++ b/src/mango.c @@ -914,6 +914,15 @@ static struct { int32_t hotspot_y; } last_cursor; +static void last_cursor_surface_destroy(struct wl_listener *listener, void *data) { + last_cursor.surface = NULL; + wl_list_remove(&listener->link); + wl_list_init(&listener->link); +} +static struct wl_listener last_cursor_surface_destroy_listener = { + .notify = last_cursor_surface_destroy +}; + #include "client/client.h" #include "config/preset.h" @@ -2141,6 +2150,11 @@ void setcursorshape(struct wl_listener *listener, void *data) { * actually has pointer focus first. If so, we can tell the cursor to * use the provided cursor shape. */ if (event->seat_client == seat->pointer_state.focused_client) { + /* Remove surface destroy listener if active */ + if (!wl_list_empty(&last_cursor_surface_destroy_listener.link)) + wl_list_remove(&last_cursor_surface_destroy_listener.link); + wl_list_init(&last_cursor_surface_destroy_listener.link); + last_cursor.shape = event->shape; last_cursor.surface = NULL; if (!cursor_hidden) @@ -4918,10 +4932,21 @@ void setcursor(struct wl_listener *listener, void *data) { * hardware cursor on the output that it's currently on and continue to * do so as the cursor moves between outputs. */ if (event->seat_client == seat->pointer_state.focused_client) { + /* Clear previous surface destroy listener if any */ + if (!wl_list_empty(&last_cursor_surface_destroy_listener.link)) + wl_list_remove(&last_cursor_surface_destroy_listener.link); + wl_list_init(&last_cursor_surface_destroy_listener.link); + last_cursor.shape = 0; last_cursor.surface = event->surface; last_cursor.hotspot_x = event->hotspot_x; last_cursor.hotspot_y = event->hotspot_y; + + /* Track surface destruction to avoid dangling pointer */ + if (event->surface) + wl_signal_add(&event->surface->events.destroy, + &last_cursor_surface_destroy_listener); + if (!cursor_hidden) wlr_cursor_set_surface(cursor, event->surface, event->hotspot_x, event->hotspot_y); @@ -5384,6 +5409,8 @@ void handle_print_status(struct wl_listener *listener, void *data) { void setup(void) { + wl_list_init(&last_cursor_surface_destroy_listener.link); + setenv("XCURSOR_SIZE", "24", 1); setenv("XDG_CURRENT_DESKTOP", "mango", 1); @@ -5818,7 +5845,7 @@ void handlecursoractivity(void) { if (last_cursor.shape) wlr_cursor_set_xcursor(cursor, cursor_mgr, wlr_cursor_shape_v1_name(last_cursor.shape)); - else + else if (last_cursor.surface) wlr_cursor_set_surface(cursor, last_cursor.surface, last_cursor.hotspot_x, last_cursor.hotspot_y); } From 20dbffdfafbdc7c4aa5fa464b3422709076667a6 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Mon, 2 Mar 2026 08:35:32 +0800 Subject: [PATCH 2/6] opt: avoid unnecessary action when layer surface commit --- src/mango.c | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/mango.c b/src/mango.c index 641e69ab..2faf35da 100644 --- a/src/mango.c +++ b/src/mango.c @@ -2476,19 +2476,26 @@ void commitlayersurfacenotify(struct wl_listener *listener, void *data) { return; l->mapped = layer_surface->surface->mapped; - if (scene_layer != l->scene->node.parent) { - wlr_scene_node_reparent(&l->scene->node, scene_layer); - wl_list_remove(&l->link); - wl_list_insert(&l->mon->layers[layer_surface->current.layer], &l->link); - wlr_scene_node_reparent( - &l->popups->node, - (layer_surface->current.layer < ZWLR_LAYER_SHELL_V1_LAYER_TOP - ? layers[LyrTop] - : scene_layer)); + if (layer_surface->current.committed & WLR_LAYER_SURFACE_V1_STATE_LAYER) { + if (scene_layer != l->scene->node.parent) { + wlr_scene_node_reparent(&l->scene->node, scene_layer); + wl_list_remove(&l->link); + wl_list_insert(&l->mon->layers[layer_surface->current.layer], + &l->link); + wlr_scene_node_reparent( + &l->popups->node, + (layer_surface->current.layer < ZWLR_LAYER_SHELL_V1_LAYER_TOP + ? layers[LyrTop] + : scene_layer)); + } + + arrangelayers(l->mon); } - arrangelayers(l->mon); - reset_exclusive_layers_focus(l->mon); + if (layer_surface->current.committed & + WLR_LAYER_SURFACE_V1_STATE_KEYBOARD_INTERACTIVITY) { + reset_exclusive_layers_focus(l->mon); + } } void commitnotify(struct wl_listener *listener, void *data) { From ad754167b7d810dd63bbfd460e80bbd30e531778 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Mon, 2 Mar 2026 09:40:50 +0800 Subject: [PATCH 3/6] fix: last_cursor surface destroy detect error --- src/mango.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/mango.c b/src/mango.c index af82733d..7c6106f9 100644 --- a/src/mango.c +++ b/src/mango.c @@ -800,6 +800,8 @@ static void monitor_stop_skip_frame_timer(Monitor *m); static int monitor_skip_frame_timeout_callback(void *data); static Monitor *get_monitor_nearest_to(int32_t lx, int32_t ly); static bool match_monitor_spec(char *spec, Monitor *m); +static void last_cursor_surface_destroy(struct wl_listener *listener, + void *data); #include "data/static_keymap.h" #include "dispatch/bind_declare.h" @@ -914,15 +916,6 @@ static struct { int32_t hotspot_y; } last_cursor; -static void last_cursor_surface_destroy(struct wl_listener *listener, void *data) { - last_cursor.surface = NULL; - wl_list_remove(&listener->link); - wl_list_init(&listener->link); -} -static struct wl_listener last_cursor_surface_destroy_listener = { - .notify = last_cursor_surface_destroy -}; - #include "client/client.h" #include "config/preset.h" @@ -973,6 +966,8 @@ static struct wl_listener new_session_lock = {.notify = locksession}; static struct wl_listener drm_lease_request = {.notify = requestdrmlease}; static struct wl_listener keyboard_shortcuts_inhibit_new_inhibitor = { .notify = handle_keyboard_shortcuts_inhibit_new_inhibitor}; +static struct wl_listener last_cursor_surface_destroy_listener = { + .notify = last_cursor_surface_destroy}; #ifdef XWAYLAND static void fix_xwayland_unmanaged_coordinate(Client *c); @@ -2159,6 +2154,11 @@ void checkidleinhibitor(struct wlr_surface *exclude) { wlr_idle_notifier_v1_set_inhibited(idle_notifier, inhibited); } +void last_cursor_surface_destroy(struct wl_listener *listener, void *data) { + last_cursor.surface = NULL; + wl_list_remove(&listener->link); +} + void setcursorshape(struct wl_listener *listener, void *data) { struct wlr_cursor_shape_manager_v1_request_set_shape_event *event = data; if (cursor_mode != CurNormal && cursor_mode != CurPressed) @@ -2168,9 +2168,9 @@ void setcursorshape(struct wl_listener *listener, void *data) { * use the provided cursor shape. */ if (event->seat_client == seat->pointer_state.focused_client) { /* Remove surface destroy listener if active */ - if (!wl_list_empty(&last_cursor_surface_destroy_listener.link)) + if (last_cursor.surface && + last_cursor_surface_destroy_listener.link.prev != NULL) wl_list_remove(&last_cursor_surface_destroy_listener.link); - wl_list_init(&last_cursor_surface_destroy_listener.link); last_cursor.shape = event->shape; last_cursor.surface = NULL; @@ -4949,9 +4949,9 @@ void setcursor(struct wl_listener *listener, void *data) { * do so as the cursor moves between outputs. */ if (event->seat_client == seat->pointer_state.focused_client) { /* Clear previous surface destroy listener if any */ - if (!wl_list_empty(&last_cursor_surface_destroy_listener.link)) + if (last_cursor.surface && + last_cursor_surface_destroy_listener.link.prev != NULL) wl_list_remove(&last_cursor_surface_destroy_listener.link); - wl_list_init(&last_cursor_surface_destroy_listener.link); last_cursor.shape = 0; last_cursor.surface = event->surface; @@ -5425,8 +5425,6 @@ void handle_print_status(struct wl_listener *listener, void *data) { void setup(void) { - wl_list_init(&last_cursor_surface_destroy_listener.link); - setenv("XCURSOR_SIZE", "24", 1); setenv("XDG_CURRENT_DESKTOP", "mango", 1); @@ -5662,6 +5660,8 @@ void setup(void) { LISTEN_STATIC(&cursor->events.hold_end, hold_end); seat = wlr_seat_create(dpy, "seat0"); + + wl_list_init(&last_cursor_surface_destroy_listener.link); wl_signal_add(&seat->events.request_set_cursor, &request_cursor); wl_signal_add(&seat->events.request_set_selection, &request_set_sel); wl_signal_add(&seat->events.request_set_primary_selection, From 46e867deb9dfd58abb22ec92b19461d3761b1f3d Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Mon, 2 Mar 2026 20:53:52 +0800 Subject: [PATCH 4/6] opt: always arrangelayers if layer commit --- src/mango.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mango.c b/src/mango.c index 7c6106f9..3bfdfb77 100644 --- a/src/mango.c +++ b/src/mango.c @@ -2502,10 +2502,10 @@ void commitlayersurfacenotify(struct wl_listener *listener, void *data) { ? layers[LyrTop] : scene_layer)); } - - arrangelayers(l->mon); } + arrangelayers(l->mon); + if (layer_surface->current.committed & WLR_LAYER_SURFACE_V1_STATE_KEYBOARD_INTERACTIVITY) { reset_exclusive_layers_focus(l->mon); From 9aa2d3cd33ce233c3986263e52163112a0c0caec Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Tue, 3 Mar 2026 13:03:35 +0800 Subject: [PATCH 5/6] opt: rename hide_source var to hide_cursor_source --- src/config/parse_config.h | 12 ++++++------ src/mango.c | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config/parse_config.h b/src/config/parse_config.h index b4fb37e9..d2946f60 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -3637,10 +3637,10 @@ void reapply_monitor_rules(void) { } void reapply_cursor_style(void) { - if (hide_source) { - wl_event_source_timer_update(hide_source, 0); - wl_event_source_remove(hide_source); - hide_source = NULL; + if (hide_cursor_source) { + wl_event_source_timer_update(hide_cursor_source, 0); + wl_event_source_remove(hide_cursor_source); + hide_cursor_source = NULL; } wlr_cursor_unset_image(cursor); @@ -3671,12 +3671,12 @@ void reapply_cursor_style(void) { wlr_cursor_set_xcursor(cursor, cursor_mgr, "left_ptr"); - hide_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), + hide_cursor_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), hidecursor, cursor); if (cursor_hidden) { wlr_cursor_unset_image(cursor); } else { - wl_event_source_timer_update(hide_source, cursor_hide_timeout * 1000); + wl_event_source_timer_update(hide_cursor_source, cursor_hide_timeout * 1000); } } diff --git a/src/mango.c b/src/mango.c index 3bfdfb77..d7245ded 100644 --- a/src/mango.c +++ b/src/mango.c @@ -898,7 +898,7 @@ struct dvec2 *baked_points_focus; struct dvec2 *baked_points_opafadein; struct dvec2 *baked_points_opafadeout; -static struct wl_event_source *hide_source; +static struct wl_event_source *hide_cursor_source; static bool cursor_hidden = false; static bool tag_combo = false; static const char *cli_config_path = NULL; @@ -5631,7 +5631,7 @@ void setup(void) { cursor_shape_mgr = wlr_cursor_shape_manager_v1_create(dpy, 1); wl_signal_add(&cursor_shape_mgr->events.request_set_shape, &request_set_cursor_shape); - hide_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), + hide_cursor_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), hidecursor, cursor); /* * Configures a seat, which is a single "seat" at which a user sits and @@ -5851,7 +5851,7 @@ void overview_restore(Client *c, const Arg *arg) { } void handlecursoractivity(void) { - wl_event_source_timer_update(hide_source, cursor_hide_timeout * 1000); + wl_event_source_timer_update(hide_cursor_source, cursor_hide_timeout * 1000); if (!cursor_hidden) return; From 1e1d41e626aa12057c03ec79ed11bcc5619f6748 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Tue, 3 Mar 2026 13:23:15 +0800 Subject: [PATCH 6/6] feat: add windowrule option indleinhibit_when_focus --- src/config/parse_config.h | 9 ++++++-- src/mango.c | 47 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/config/parse_config.h b/src/config/parse_config.h index d2946f60..8a965360 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -78,6 +78,7 @@ typedef struct { int32_t ignore_maximize; int32_t ignore_minimize; int32_t isnosizehint; + int32_t indleinhibit_when_focus; const char *monitor; int32_t offsetx; int32_t offsety; @@ -2022,6 +2023,7 @@ bool parse_option(Config *config, char *key, char *value) { rule->ignore_maximize = -1; rule->ignore_minimize = -1; rule->isnosizehint = -1; + rule->indleinhibit_when_focus = -1; rule->isterm = -1; rule->allow_csd = -1; rule->force_maximize = -1; @@ -2132,6 +2134,8 @@ bool parse_option(Config *config, char *key, char *value) { rule->ignore_minimize = atoi(val); } else if (strcmp(key, "isnosizehint") == 0) { rule->isnosizehint = atoi(val); + } else if (strcmp(key, "indleinhibit_when_focus") == 0) { + rule->indleinhibit_when_focus = atoi(val); } else if (strcmp(key, "isterm") == 0) { rule->isterm = atoi(val); } else if (strcmp(key, "allow_csd") == 0) { @@ -3672,11 +3676,12 @@ void reapply_cursor_style(void) { wlr_cursor_set_xcursor(cursor, cursor_mgr, "left_ptr"); hide_cursor_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), - hidecursor, cursor); + hidecursor, cursor); if (cursor_hidden) { wlr_cursor_unset_image(cursor); } else { - wl_event_source_timer_update(hide_cursor_source, cursor_hide_timeout * 1000); + wl_event_source_timer_update(hide_cursor_source, + cursor_hide_timeout * 1000); } } diff --git a/src/mango.c b/src/mango.c index d7245ded..8a93a792 100644 --- a/src/mango.c +++ b/src/mango.c @@ -347,7 +347,7 @@ struct Client { struct wlr_foreign_toplevel_handle_v1 *foreign_toplevel; int32_t isfloating, isurgent, isfullscreen, isfakefullscreen, need_float_size_reduce, isminimized, isoverlay, isnosizehint, - ignore_maximize, ignore_minimize; + ignore_maximize, ignore_minimize, indleinhibit_when_focus; int32_t ismaximizescreen; int32_t overview_backup_bw; int32_t fullscreen_backup_x, fullscreen_backup_y, fullscreen_backup_w, @@ -802,6 +802,8 @@ static Monitor *get_monitor_nearest_to(int32_t lx, int32_t ly); static bool match_monitor_spec(char *spec, Monitor *m); static void last_cursor_surface_destroy(struct wl_listener *listener, void *data); +static int32_t keep_idle_inhibit(void *data); +static void check_keep_idle_inhibit(Client *c); #include "data/static_keymap.h" #include "dispatch/bind_declare.h" @@ -899,6 +901,7 @@ struct dvec2 *baked_points_opafadein; struct dvec2 *baked_points_opafadeout; static struct wl_event_source *hide_cursor_source; +static struct wl_event_source *keep_idle_inhibit_source; static bool cursor_hidden = false; static bool tag_combo = false; static const char *cli_config_path = NULL; @@ -1335,6 +1338,7 @@ static void apply_rule_properties(Client *c, const ConfigWinRule *r) { APPLY_INT_PROP(c, r, ignore_maximize); APPLY_INT_PROP(c, r, ignore_minimize); APPLY_INT_PROP(c, r, isnosizehint); + APPLY_INT_PROP(c, r, indleinhibit_when_focus); APPLY_INT_PROP(c, r, isunglobal); APPLY_INT_PROP(c, r, noblur); APPLY_INT_PROP(c, r, allow_shortcuts_inhibit); @@ -3482,6 +3486,8 @@ void focusclient(Client *c, int32_t lift) { selmon->sel = c; c->isfocusing = true; + check_keep_idle_inhibit(c); + if (last_focus_client && !last_focus_client->iskilling && last_focus_client != c) { last_focus_client->isfocusing = false; @@ -4029,6 +4035,7 @@ void init_client_properties(Client *c) { c->force_tiled_state = 1; c->force_tearing = 0; c->allow_shortcuts_inhibit = SHORTCUTS_INHIBIT_ENABLE; + c->indleinhibit_when_focus = 0; c->scroller_proportion_single = 0.0f; c->float_geom.width = 0; c->float_geom.height = 0; @@ -5564,6 +5571,9 @@ void setup(void) { idle_inhibit_mgr = wlr_idle_inhibit_v1_create(dpy); wl_signal_add(&idle_inhibit_mgr->events.new_inhibitor, &new_idle_inhibitor); + keep_idle_inhibit_source = wl_event_loop_add_timer( + wl_display_get_event_loop(dpy), keep_idle_inhibit, NULL); + layer_shell = wlr_layer_shell_v1_create(dpy, 4); wl_signal_add(&layer_shell->events.new_surface, &new_layer_surface); @@ -5632,7 +5642,7 @@ void setup(void) { wl_signal_add(&cursor_shape_mgr->events.request_set_shape, &request_set_cursor_shape); hide_cursor_source = wl_event_loop_add_timer(wl_display_get_event_loop(dpy), - hidecursor, cursor); + hidecursor, cursor); /* * Configures a seat, which is a single "seat" at which a user sits and * operates the computer. This conceptually includes up to one keyboard, @@ -5851,7 +5861,8 @@ void overview_restore(Client *c, const Arg *arg) { } void handlecursoractivity(void) { - wl_event_source_timer_update(hide_cursor_source, cursor_hide_timeout * 1000); + wl_event_source_timer_update(hide_cursor_source, + cursor_hide_timeout * 1000); if (!cursor_hidden) return; @@ -5872,6 +5883,36 @@ int32_t hidecursor(void *data) { return 1; } +void check_keep_idle_inhibit(Client *c) { + if (c && c->indleinhibit_when_focus && keep_idle_inhibit_source) { + wl_event_source_timer_update(keep_idle_inhibit_source, 1000); + } +} + +int32_t keep_idle_inhibit(void *data) { + + if (!idle_inhibit_mgr) { + wl_event_source_timer_update(keep_idle_inhibit_source, 0); + return 1; + } + + if (session && !session->active) { + wl_event_source_timer_update(keep_idle_inhibit_source, 0); + return 1; + } + + if (!selmon || !selmon->sel || !selmon->sel->indleinhibit_when_focus) { + wl_event_source_timer_update(keep_idle_inhibit_source, 0); + return 1; + } + + if (seat && idle_notifier) { + wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); + wl_event_source_timer_update(keep_idle_inhibit_source, 1000); + } + return 1; +} + void unlocksession(struct wl_listener *listener, void *data) { SessionLock *lock = wl_container_of(listener, lock, unlock); destroylock(lock, 1);