Skip to content

fix(clipboard): skip X11 control targets and bound each MIME read - #36

Merged
Nadim147c merged 2 commits into
Nadim147c:mainfrom
WayneKent:fix/skip-x11-control-targets
Oct 29, 2026
Merged

Nadim147c merged 2 commits into
Nadim147c:mainfrom
WayneKent:fix/skip-x11-control-targets

Conversation

@WayneKent

Copy link
Copy Markdown
Contributor

Problem

retrieveData reads every MIME type the offer advertises, sequentially, by
calling receive and then reading the pipe to EOF. Two things go wrong with
that:

1. Some advertised "MIME types" are not content. GTK advertises X11 ICCCM
control targets such as SAVE_TARGETS (the clipboard-manager save handshake),
TARGETS and TIMESTAMP. For SAVE_TARGETS the source never writes to the fd
and never closes it, so io.ReadAll blocks forever. Because the whole parse
runs inside the Wayland event callback, the watcher stops dispatching until the
source is destroyed — which only happens on the next selection change.

Symptom: wl-paste and Ctrl+V show the new clipboard content immediately, but
the entry only appears in yankd list after you copy something else. On the
unfixed build nothing is logged: the read is simply stuck.

2. One unreadable MIME type dropped the whole event. parse returned the
error on the first failing MIME type, discarding the event even though every
other MIME type had been read successfully.

Change

  • Add a controlTargets set — SAVE_TARGETS, TARGETS, TIMESTAMP,
    MULTIPLE, DELETE, INSERT_SELECTION, INSERT_PROPERTY, LENGTH — and
    skip those next to the existing isBadMime filter, before the pipe is opened.
  • The uppercase X11 data targets (STRING, UTF8_STRING, TEXT,
    COMPOUND_TEXT) are deliberately not in the set: they are content rather
    than a handshake. UTF8_STRING is the text as UTF-8; STRING / TEXT are the
    same text with non-ASCII escaped as \uXXXX. Skipping every type whose name
    starts with an uppercase letter — the shortcut another clipboard manager
    (CopyQ) uses to dodge SAVE_TARGETS — would silently drop that content, and
    yankd has no other path that recovers it.
  • Give each MIME read an idle timeout (mimeReadTimeout = 1s), with the
    deadline reset before every read, so a payload that keeps streaming is never
    cut off, while a target that goes silent is abandoned.
  • A MIME type that fails to read is now skipped instead of dropping the event;
    retrieveData already logs the error, so the read failure is still visible
    in the daemon log.

Tests

TestReadWithIdleTimeout covers the new read loop:

  • a source that keeps sending with gaps shorter than the idle timeout but a
    total run longer than it (so the timeout must be idle-based, not total)
  • a source that never sends anything (must return a deadline error, not block)
  • a source that sends part of its data and then goes silent (partial data is
    returned with an error, so the caller discards it)

The "skip a failed MIME type" path needs a live offer object and has no unit
test; it was verified by running the daemon and copying from a browser.

Reproducing on the unfixed version

Whether this reproduces depends on the source: the extra target has to be
advertised, and only some clients do that. Firefox on Wayland does: it is a
GTK3/GDK application (it loads libgtk-3 / libgdk-3 and runs with
GDK_BACKEND=wayland), and the offers it makes carry the X11 ICCCM
compatibility names (SAVE_TARGETS, TARGETS, TIMESTAMP) next to
STRING / TEXT / COMPOUND_TEXT. foot and wl-copy implement
wl_data_source directly and never advertise those, which is why copying from
them never hangs. Other GTK applications behave like Firefox.

# 1. In Firefox on Wayland, select some text and press Ctrl+C, or use a page's
#    copy button.

# 2. Check whether the current offer advertises the target that hangs the read:
wl-paste --list-types | grep -x SAVE_TARGETS \
  && echo "this offer reproduces it on the unfixed build" \
  || echo "not this time — copy again from Firefox"

When it is listed, on the unfixed build copying from Firefox produces no
daemon log line and no yankd list entry
until you copy something else; with
this change the entry lands immediately and the target is skipped before the
pipe is opened.

(The presence of SAVE_TARGETS is not constant across copies — that is why the
check above decides per offer instead of being assumed.)

After the change, an unknown target that never answers is abandoned after one
second and logged as

ERRO failed to read data mime=<target> error="idle timeout after 1s with 0 bytes read: read |0: i/o timeout"

- add a controlTargets blacklist: SAVE_TARGETS, TARGETS, TIMESTAMP,
  MULTIPLE, DELETE, INSERT_SELECTION, INSERT_PROPERTY, LENGTH
- filter them in parse() next to isBadMime(), before retrieveData opens
  the pipe
- GTK advertises SAVE_TARGETS but never writes to the fd nor closes it,
  which blocked the pipe read forever and stalled the whole watcher until
  the next selection change
- uppercase X11 data targets (STRING, UTF8_STRING, TEXT, COMPOUND_TEXT)
  are intentionally kept: they carry real text
- read each MIME type with an idle timeout instead of an unbounded
  io.ReadAll: the deadline is reset before every read, so a payload that
  keeps streaming is never cut off
- skip a MIME type that fails to read and keep the rest of the event,
  instead of discarding the whole clipboard event
- wrap deadline errors with the elapsed timeout and bytes read, so the log
  reads "idle timeout after 1s with 0 bytes read" instead of a bare
  "i/o timeout"
- add TestReadWithIdleTimeout: slow-but-continuous source, silent source,
  partial read
@Nadim147c

Copy link
Copy Markdown
Owner

Sorry I don't know why but I did not get any notification for this PR.

@Nadim147c

Copy link
Copy Markdown
Owner

Thanks for the PR!

@Nadim147c
Nadim147c merged commit 9bc4cb8 into Nadim147c:main Oct 29, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants