From 98366076e1da0fdda7e1486b4dba074ac51905b3 Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:40:45 +0000 Subject: [PATCH] fix(wayland): detect start_drag silently ignored by the compositor The pointer_button_count check added in dc36e2165 uses the client side view of the implicit grab, which is stale when the button release is still in flight: glfwStartDrag passes the check, but by the time the compositor processes wl_data_device.start_drag the implicit grab is gone and the request is silently dropped. The data source then never receives any event, so the tab drag state leaks forever, hijacking mouse handling again, and the already created xdg_toplevel_drag toplevel gets mapped as a stray undecorated window showing the drag thumbnail that nothing ever destroys. Detect this deterministically using protocol ordering: issue a wl_display.sync right after start_drag. An accepted start_drag synchronously produces events (wl_pointer.leave from the DND grab taking over, wl_data_device.enter, wl_data_source events) that are ordered before the sync callback. If the callback fires with none of them seen, the request was dropped: cancel the drag, which notifies the application (clearing the tab drag state) and destroys the drag toplevel and data source. Also defer mapping the drag toplevel until the session is confirmed, so the stray window can never appear, not even for one frame. Verified against headless GNOME Shell 50.2 (mutter) with injected pointer timing scans: the unpatched build deadlocks with an orphaned drag toplevel within ~13 fast flicks, the patched build catches every race hit and cancels cleanly, with no deadlock, no stray window, and normal slow drag/reorder/detach behaviour preserved. Co-Authored-By: Claude Fable 5 Co-authored-by: Cursor --- glfw/wl_init.c | 3 ++ glfw/wl_platform.h | 15 +++++++++ glfw/wl_window.c | 78 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/glfw/wl_init.c b/glfw/wl_init.c index 6741fa528..08d2a1a5e 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -107,6 +107,9 @@ pointerHandleLeave(void* data UNUSED, struct wl_pointer* pointer UNUSED, uint32_ // over the pointer for drag-and-drop). The matching button releases will // never be delivered to us. _glfw.wl.pointer_button_count = 0; + // A DND grab taking over the pointer sends a leave, making this the + // earliest proof that a just requested start_drag was accepted. + _glfwWaylandConfirmDragSession(); _GLFWwindow* window = _glfw.wl.pointerFocus; if (!window) return; _glfw.wl.serial = serial; diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index af17d12a4..163ec103f 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -434,6 +434,20 @@ typedef struct _GLFWlibraryWayland struct xdg_surface *toplevel_xdg_surface; struct xdg_toplevel *toplevel_xdg_toplevel; struct wl_buffer *toplevel_buffer; + // wl_data_device.start_drag is silently ignored by compositors when + // its serial does not match an active pointer implicit grab, which + // can happen as drags are started asynchronously and the client side + // view of the grab can be stale. A wl_display.sync issued right after + // start_drag detects this: any compositor event proving the DND + // session is live (wl_pointer.leave, wl_data_device.enter, any + // wl_data_source event) is ordered before the sync callback, so if + // the callback fires first the start_drag was dropped. + struct wl_callback *start_confirmation; + bool session_confirmed; + // The drag toplevel was configured before the session was confirmed, + // mapping it was deferred so it cannot end up as a stray regular + // window if start_drag was silently ignored. + bool toplevel_map_deferred; struct { const char *mime_type; int fd; @@ -487,6 +501,7 @@ void animateCursorImage(id_type timer_id, void *data); struct wl_cursor* _glfwLoadCursor(GLFWCursorShape, struct wl_cursor_theme*); void destroy_data_offer(_GLFWWaylandDataOffer*); const char* _glfwWaylandCompositorName(void); +void _glfwWaylandConfirmDragSession(void); typedef struct wayland_cursor_shape { int which; const char *name; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index abb586f41..cb3f688f4 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -2561,6 +2561,8 @@ update_drop_source_actions(_GLFWwindow *window, _GLFWWaylandDataOffer *offer) { static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id) { debug_input("Drop entered\n"); + // an enter for a drag we started means the DND session is live + _glfwWaylandConfirmDragSession(); _GLFWWaylandDataOffer *offer = &_glfw.wl.drop_data_offer; mark_data_offer(offer, id); if (!offer->id) return; @@ -3164,9 +3166,7 @@ GLFWAPI bool glfwWaylandBeep(GLFWwindow *handle) { // Drag source {{{ static void -drag_toplevel_xdg_surface_configure(void *data UNUSED, struct xdg_surface *surface, uint32_t serial) { - debug_input("Drag toplevel surface configured\n"); - xdg_surface_ack_configure(surface, serial); +map_drag_toplevel(void) { struct wl_buffer *buf = _glfw.wl.drag.toplevel_buffer; if (buf) { wl_surface_attach(_glfw.wl.drag.drag_icon, buf, 0, 0); @@ -3178,6 +3178,23 @@ drag_toplevel_xdg_surface_configure(void *data UNUSED, struct xdg_surface *surfa if (buf) wl_buffer_destroy(buf); } +static void +drag_toplevel_xdg_surface_configure(void *data UNUSED, struct xdg_surface *surface, uint32_t serial) { + debug_input("Drag toplevel surface configured\n"); + xdg_surface_ack_configure(surface, serial); + if (_glfw.wl.drag.start_confirmation && !_glfw.wl.drag.session_confirmed) { + // The compositor has not yet proven that it accepted start_drag. If + // it silently ignored it (stale implicit grab serial), committing a + // buffer now would map the toplevel as a stray regular window that + // nothing ever destroys. Defer mapping until the session is + // confirmed; if it never is, the toplevel is destroyed unmapped. + debug_input("Deferring drag toplevel map until drag session is confirmed\n"); + _glfw.wl.drag.toplevel_map_deferred = true; + return; + } + map_drag_toplevel(); +} + static const struct xdg_surface_listener drag_toplevel_xdg_surface_listener = { .configure = drag_toplevel_xdg_surface_configure, }; @@ -3213,6 +3230,46 @@ cancel_drag2(GLFWDragEventType type, bool maybe_a_cancel) { static void cancel_drag(GLFWDragEventType type) { cancel_drag2(type, false); } +void +_glfwWaylandConfirmDragSession(void) { + // Called on receipt of any compositor event that can only happen when a + // DND session is actually live: the start_drag was not silently dropped. + if (!_glfw.wl.drag.source || _glfw.wl.drag.session_confirmed) return; + _glfw.wl.drag.session_confirmed = true; + debug_input("Drag session confirmed as started by compositor\n"); + if (_glfw.wl.drag.toplevel_map_deferred) { + _glfw.wl.drag.toplevel_map_deferred = false; + map_drag_toplevel(); + } +} + +static void +drag_start_confirmation_handle_done(void *data UNUSED, struct wl_callback *callback, uint32_t cb_data UNUSED) { + if (callback != _glfw.wl.drag.start_confirmation) { + // stale callback from a drag that was already cleaned up + wl_callback_destroy(callback); + return; + } + _glfw.wl.drag.start_confirmation = NULL; + wl_callback_destroy(callback); + if (!_glfw.wl.drag.session_confirmed) { + // The compositor processed start_drag before this sync callback, and + // an accepted start_drag synchronously produces events (pointer + // leave, data device enter, data source events) that are ordered + // before it. None arrived, so the compositor silently ignored + // start_drag (no active implicit grab matching the serial). Without + // this, the data source would never receive any event, leaking the + // drag state forever and orphaning the drag toplevel as a stray + // window. + _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: start_drag was silently ignored by the compositor, cancelling drag"); + cancel_drag(GLFW_DRAG_CANCELLED); + } +} + +static const struct wl_callback_listener drag_start_confirmation_listener = { + .done = drag_start_confirmation_handle_done, +}; + #define dr _glfw.wl.drag.data_requests[i] static void @@ -3387,6 +3444,7 @@ drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const static void drag_source_target(void *data UNUSED, struct wl_data_source *source UNUSED, const char *mime_type) { debug_input("Drag source accepted MIME type: %s\n", mime_type); + _glfwWaylandConfirmDragSession(); _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); if (window) { GLFWDragEvent ev = {.type=GLFW_DRAG_ACCEPTED, .mime_type=mime_type}; @@ -3397,6 +3455,7 @@ drag_source_target(void *data UNUSED, struct wl_data_source *source UNUSED, cons static void drag_source_action(void *data UNUSED, struct wl_data_source *source UNUSED, uint32_t dnd_action) { debug_input("Drag source action changed: %d\n", dnd_action); + _glfwWaylandConfirmDragSession(); _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); if (window) { GLFWDragOperationType op = GLFW_DRAG_OPERATION_GENERIC; @@ -3414,6 +3473,7 @@ drag_source_action(void *data UNUSED, struct wl_data_source *source UNUSED, uint static void drag_source_dnd_drop_performed(void *data UNUSED, struct wl_data_source *source UNUSED) { debug_input("Drag source drop performed\n"); + _glfwWaylandConfirmDragSession(); _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); if (window) { GLFWDragEvent ev = {.type=GLFW_DRAG_DROPPED}; @@ -3465,6 +3525,7 @@ _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) { void _glfwPlatformFreeDragSourceData(void) { + if (_glfw.wl.drag.start_confirmation) wl_callback_destroy(_glfw.wl.drag.start_confirmation); if (_glfw.wl.drag.drag_viewport) wp_viewport_destroy(_glfw.wl.drag.drag_viewport); if (_glfw.wl.drag.toplevel_drag) xdg_toplevel_drag_v1_destroy(_glfw.wl.drag.toplevel_drag); if (_glfw.wl.drag.toplevel_buffer) wl_buffer_destroy(_glfw.wl.drag.toplevel_buffer); @@ -3594,6 +3655,17 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) { if (icon_buffer) wl_buffer_destroy(icon_buffer); + // The pointer_button_count check above uses the client side view of the + // implicit grab, which can be stale: the button may already be released + // with the release event still in flight, in which case the compositor + // silently ignores start_drag and the data source never receives any + // event. Detect that with a sync: an accepted start_drag synchronously + // produces events ordered before the sync callback. + _glfw.wl.drag.session_confirmed = false; + _glfw.wl.drag.start_confirmation = wl_display_sync(_glfw.wl.display); + if (_glfw.wl.drag.start_confirmation) + wl_callback_add_listener(_glfw.wl.drag.start_confirmation, &drag_start_confirmation_listener, NULL); + return 0; } // }}}