#2902·glfw

Wayland: NULL wl_pointer dereference in setCursorImage when seat drops pointer capability while an animated cursor is shown

Author: cdelguercioCreated Sep 16, 2026Updated Sep 16, 2026

Environment

  • GLFW 3.4.0 (also present on current master, 92dcf4c)
  • Ubuntu 26.04, GNOME/Mutter Wayland session, libwayland-client 1.24.0
  • Statically linked into a closed-source application; crash reproduced from an Apport core dump

Summary

When the compositor removes WL_SEAT_CAPABILITY_POINTER from the seat (Mutter does this whenever the last pointer-capable device is removed, e.g. unplugging the only USB mouse or a Bluetooth mouse going to sleep), seatHandleCapabilities destroys _glfw.wl.pointer and sets it to NULL but leaves the rest of the pointer state untouched. If an animated cursor was active at that moment, the cursor timerfd is still armed, and on the next tick handleEvents calls incrementCursorImage -> setCursorImage -> wl_pointer_set_cursor(NULL, ...), which segfaults inside wl_proxy_marshal.

Backtrace

0 wl_proxy_marshal () from libwayland-client.so.0 (proxy == NULL, opcode == WL_POINTER_SET_CURSOR) 1 setCursorImage (src/wl_window.c, wl_pointer_set_cursor call) 2 incrementCursorImage 3 handleEvents (cursorTimerfd POLLIN branch) 4 _glfwPollEventsWayland 5 application main loop Registers at the fault: rdi=0 (proxy), rsi=0 (opcode), rdx=<pointer enter serial>, r8/r9=<hotspot>.

Analysis (3.4 line numbers, master equivalents in parentheses)

  • seatHandleCapabilities (wl_window.c:1880, master:2072): on capability loss it does wl_pointer_destroy(_glfw.wl.pointer); _glfw.wl.pointer = NULL; and nothing else. _glfw.wl.pointerFocus (master: _glfw.wl.pointerSurface), window->wl.hovered and _glfw.wl.cursorTimerfd all keep their previous state. The compositor does not send wl_pointer.leave before the capability change, so nothing else clears them.
  • handleEvents (wl_window.c:1288, master:1472): when the cursor timerfd fires it calls incrementCursorImage(_glfw.wl.pointerFocus) (master: incrementCursorImage() which reads _glfw.wl.pointerSurface). The stale focus passes the checks.
  • setCursorImage (wl_window.c:1078, master:1248) re-arms the timer and calls wl_pointer_set_cursor(_glfw.wl.pointer, ...) with _glfw.wl.pointer == NULL.

The timer is only armed for cursors whose wl_cursor_image.delay is non-zero, so a static arrow never triggers this; an animated theme cursor (e.g. "wait"/"progress") is required.

Steps to reproduce

  1. GNOME Wayland session with exactly one pointer device (e.g. one USB mouse, no touchpad).
  2. Run any GLFW 3.4 program and make it show an animated cursor (a glfwCreateCursor cursor with multiple images, or a theme where the chosen standard cursor is animated) while the mouse hovers the window.
  3. Unplug the mouse.
  4. The program expected to crash in wl_proxy_marshal within one cursor frame delay.

Workaround for my application

Running with these env changes fixed this issue:

env -u WAYLAND_DISPLAY XDG_SESSION_TYPE=x11 my-app

Suggested fix

In seatHandleCapabilities, when destroying/releasing the pointer, also:

  • disarm the cursor timer (timerfd_settime with a zeroed itimerspec),
  • clear _glfw.wl.pointerFocus / _glfw.wl.pointerSurface,
  • if the focused window had wl.hovered set, clear it and emit _glfwInputCursorEnter(window, GLFW_FALSE) so the application sees a cursor leave, mirroring pointerHandleLeave.

Additionally, setCursorImage (and the other wl_pointer_set_cursor call sites) could early-return when _glfw.wl.pointer == NULL as a defensive check.