From 8a46e92e03246ae69ee0da88069e563978a190e2 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Tue, 11 Aug 2026 19:07:02 +1000 Subject: [PATCH] Fix clipboard echo flood: keep pending-echo guards across focus blips The nested wl_data_device selection handler treated the host's own echo of our set_selection as a real selection whenever a focus blip (dd selection with a NULL offer) had cleared the pending-echo flags first. That echoed our selection back to the host and started a self-feeding set_selection / dc_source_send loop that never converged and never timed out. - bridge_dd_selection: a NULL offer is a focus blip - keep the last-known selection and leave the pending-echo flags intact instead of clearing them and mirroring back. - dc/dd echo swallow: clear the matching pending-echo flag when we swallow our own echo (previously only the dc path reset anything). - host_kbd_enter: only reset the pending-echo flags for the focus-based wl_data_device backend, where a stale serial can orphan a guard. The data-control backend echoes independently of focus, so its in-flight guard must survive keyboard-enter events. --- tabbed-wlroots.c | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/tabbed-wlroots.c b/tabbed-wlroots.c index 512a9a1..0de45b9 100644 --- a/tabbed-wlroots.c +++ b/tabbed-wlroots.c @@ -1430,8 +1430,8 @@ static void bridge_dc_device_selection(void *data, if (offer && offer == bridge.dc_echo_offer) { bridge.dc_echo_offer = NULL; bridge_mimes_reset(); - if (offer) - zwlr_data_control_offer_v1_destroy(offer); + zwlr_data_control_offer_v1_destroy(offer); + bridge.dc_pending_echo = false; return; } bridge.dc_pending_echo = false; @@ -1516,12 +1516,20 @@ static void bridge_dd_data_offer(void *data, struct wl_data_device *dev, static void bridge_dd_selection(void *data, struct wl_data_device *dev, struct wl_data_offer *offer) { (void)data; (void)dev; + if (!offer) { + /* Focus blip: the host selection is momentarily gone (or we lost + keyboard focus). Keep the last-known nested selection and, above + all, leave the pending-echo flags intact so an in-flight dc/dd + set_selection reply is still recognized as our own echo. */ + bridge_mimes_reset(); + return; + } bridge_prefetch_cancel(); - if (offer && offer == bridge.dd_echo_offer) { + if (offer == bridge.dd_echo_offer) { bridge.dd_echo_offer = NULL; bridge_mimes_reset(); - if (offer) - wl_data_offer_destroy(offer); + wl_data_offer_destroy(offer); + bridge.dd_pending_echo = false; return; } bridge.dc_pending_echo = false; @@ -1529,12 +1537,6 @@ static void bridge_dd_selection(void *data, struct wl_data_device *dev, if (bridge.dd_offer) wl_data_offer_destroy(bridge.dd_offer); bridge.dd_offer = offer; - if (!offer) { - /* selection cleared, or we lost keyboard focus: keep the last-known - nested selection rather than emptying it on every focus change */ - bridge_mimes_reset(); - return; - } bridge_prefetch_start(); bridge_mirror_to_nested(); } @@ -1889,11 +1891,18 @@ static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { (void)data; (void)kbd; (void)surface; (void)keys; - /* Remember the enter serial for host clipboard set_selection, and drop any - stale echo guard from a set_selection made while unfocused. */ + /* Remember the enter serial for host clipboard set_selection. Only the + focus-based wl_data_device backend can hold a stale echo guard (a + set_selection sent with a no-longer-valid serial gets no reply): drop it + on refocus there. The data-control backend echoes independently of + focus, so its in-flight guard must be preserved here, or the host's echo + of our own set_selection is mistaken for a real selection and mirrored + back into an endless set_selection/receive loop. */ bridge.kbd_serial = serial; - bridge.dc_pending_echo = false; - bridge.dd_pending_echo = false; + if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE) { + bridge.dc_pending_echo = false; + bridge.dd_pending_echo = false; + } if (bridge.host_set_pending && server.seat->selection_source && server.seat->selection_source->impl != &bridge_inner_source_impl) { bridge.host_set_pending = false;