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.
This commit is contained in:
Andrew Hamilton 2026-08-11 19:07:02 +10:00
parent ad46508962
commit 8a46e92e03

View file

@ -1430,8 +1430,8 @@ static void bridge_dc_device_selection(void *data,
if (offer && offer == bridge.dc_echo_offer) { if (offer && offer == bridge.dc_echo_offer) {
bridge.dc_echo_offer = NULL; bridge.dc_echo_offer = NULL;
bridge_mimes_reset(); 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; return;
} }
bridge.dc_pending_echo = false; 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, static void bridge_dd_selection(void *data, struct wl_data_device *dev,
struct wl_data_offer *offer) { struct wl_data_offer *offer) {
(void)data; (void)dev; (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(); bridge_prefetch_cancel();
if (offer && offer == bridge.dd_echo_offer) { if (offer == bridge.dd_echo_offer) {
bridge.dd_echo_offer = NULL; bridge.dd_echo_offer = NULL;
bridge_mimes_reset(); bridge_mimes_reset();
if (offer) wl_data_offer_destroy(offer);
wl_data_offer_destroy(offer); bridge.dd_pending_echo = false;
return; return;
} }
bridge.dc_pending_echo = false; 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) if (bridge.dd_offer)
wl_data_offer_destroy(bridge.dd_offer); wl_data_offer_destroy(bridge.dd_offer);
bridge.dd_offer = 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_prefetch_start();
bridge_mirror_to_nested(); 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) struct wl_surface *surface, struct wl_array *keys)
{ {
(void)data; (void)kbd; (void)surface; (void)keys; (void)data; (void)kbd; (void)surface; (void)keys;
/* Remember the enter serial for host clipboard set_selection, and drop any /* Remember the enter serial for host clipboard set_selection. Only the
stale echo guard from a set_selection made while unfocused. */ 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.kbd_serial = serial;
bridge.dc_pending_echo = false; if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE) {
bridge.dd_pending_echo = false; bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
}
if (bridge.host_set_pending && server.seat->selection_source && if (bridge.host_set_pending && server.seat->selection_source &&
server.seat->selection_source->impl != &bridge_inner_source_impl) { server.seat->selection_source->impl != &bridge_inner_source_impl) {
bridge.host_set_pending = false; bridge.host_set_pending = false;