Merge branch 'fix-wayland-start-drag-silently-ignored' of https://github.com/ttys3/kitty
Some checks are pending
CI / Linux (python=3.13 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.11 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run

This commit is contained in:
Kovid Goyal 2026-06-18 10:37:36 +05:30
commit 285c576ddd
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 93 additions and 3 deletions

3
glfw/wl_init.c vendored
View file

@ -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;

15
glfw/wl_platform.h vendored
View file

@ -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;

78
glfw/wl_window.c vendored
View file

@ -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;
}
// }}}