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.
This commit is contained in:
Han Boetes 2026-03-01 21:58:03 +01:00 committed by DreamMaoMao
parent 4b35b3b4a9
commit 9a43368279

View file

@ -935,6 +935,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"
@ -2180,6 +2189,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)
@ -4932,10 +4946,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);
@ -5398,6 +5423,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);
@ -5841,7 +5868,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);
}