Skip to content

Fix late device-attributes response leaking to the active pane#5128

Open
chris-monardo wants to merge 1 commit into
tmux:masterfrom
chris-monardo:chrism/da-timer-flag-split
Open

Fix late device-attributes response leaking to the active pane#5128
chris-monardo wants to merge 1 commit into
tmux:masterfrom
chris-monardo:chrism/da-timer-flag-split

Conversation

@chris-monardo

@chris-monardo chris-monardo commented May 27, 2026

Copy link
Copy Markdown

Fixes #5129.

Summary

tty_start_timer_callback (tty.c:304) unconditionally OR's in TTY_ALL_REQUEST_FLAGS — i.e. TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA — when the 5 s query timer fires. Those same bits are the "we have already parsed one" gate in the DA response parsers, so a legitimate DA response arriving later than 5 s is rejected by the parsers and falls through to the generic key parser, leaking bytes like ?61;4;6;...c into the active pane. The original report (Windows Terminal over SSH on first attach) is exactly the case where the response lands late. See #5129 for the standalone bug report and a reproducer.

Changes

Split the flag's two meanings:

  • TTY_HAVEDA / TTY_HAVEDA2 / TTY_HAVEXDA stay as "we parsed one" — set only on a successful parse, as before.
  • New TTY_DA_QUERIESDONE means "stop waiting" — set by tty_start_timer_callback (and by the non-VT100LIKE shortcut in tty_send_requests).
  • tty_send_requests gates each query on (TTY_HAVE* | TTY_DA_QUERIESDONE) so the timer still suppresses re-querying.
  • The partial-key escape-delay check no longer treats the timer-set bits as "responses received".
  • DA response parsers (tty_keys_device_attributes etc.) are unchanged — late responses now reach them with TTY_HAVEDA unset and parse normally, including the feature update and redraw.

tty_start_timer_callback unconditionally OR'd in TTY_ALL_REQUEST_FLAGS
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA) when the 5s query timer fired.
That same bit was also the "we have already parsed one" gate in the DA
response parsers, so a legitimate response arriving later than 5s --
common over slow SSH on first attach -- was rejected by the parsers
and fell through to the generic key parser, leaking bytes like
?61;4;6;...c into the active pane.

Introduce a separate TTY_DA_QUERIESDONE flag for "stop waiting" and
leave TTY_HAVEDA/TTY_HAVEDA2/TTY_HAVEXDA to mean strictly "we parsed
one". tty_start_timer_callback sets only TTY_DA_QUERIESDONE;
tty_send_requests gates each query on (TTY_HAVE* | TTY_DA_QUERIESDONE);
the partial-key escape-delay check no longer treats the timer-set flags
as "responses received". DA response parsers are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chris-monardo chris-monardo force-pushed the chrism/da-timer-flag-split branch from e42682d to f864943 Compare May 27, 2026 20:26
@nicm

nicm commented May 27, 2026

Copy link
Copy Markdown
Member

I don't get it. We send all the requests together, so if the timer fires, don't we want to give up on them all?

@chris-monardo

Copy link
Copy Markdown
Author

The leak path I'm trying to fix:

  1. The 5 s timer fires in tty_start_timer_callback and OR's in TTY_ALL_REQUEST_FLAGS.
  2. A real DA response arrives later. tty_keys_device_attributes sees TTY_HAVEDA set and return -1.
  3. tty_keys_next reads -1 as "not a DA, try the next handler." No other handler matches \033[?...c.
  4. The bytes fall through to the generic key parser at the bottom of tty_keys_next, which splits them into per-byte key events and forwards them to the active pane.

So the timer's "give up on them all" is fine in spirit — the issue is that return -1 inside the parsers doesn't discard, it routes the bytes onward to the key handler.

Two ways to plug it:

  • What this PR currently does: split the timer flag off (TTY_DA_QUERIESDONE) so TTY_HAVE* stays strictly post-parse. Late DA-shaped bytes match the parser's format check and get consumed there. Side effect: late feature info is also processed.
  • Smaller alternative: keep the timer setting TTY_HAVE*, change the parsers' early-exit from return -1 to consume-and-discard (format-validate + return 0). One-file change, no new flag, late feature info is dropped.

Both fix the leak. Which would you prefer?

@chris-monardo

Copy link
Copy Markdown
Author

To put a finer point on the choice — it really comes down to how the 5 s timer is meant to be read.

Its mechanics today are an "unblock": it lets three things that were waiting move forward — commit to default features so the first draw can proceed, stop `tty_send_requests` from re-issuing queries, and return the partial-key escape-delay to normal. None of those require rejecting a response that does eventually arrive. `tty_update_features` already anticipates the case:

Features might have changed since the first draw during attach. For example, this happens when DA responses are received.

So:

  • Timer = unblock → late responses are fine, parsed normally, features update via the existing post-attach mechanism (one redraw at the moment of update). That's the split-flag shape this PR currently has.
  • Timer = hard stop on DA → late responses are dropped, no late redraw. That's the consume-and-discard shape.

Both fix the leak. It's a policy call: correct feature detection for slow terminals at the cost of a possible late redraw, vs. commit to defaults at 5 s and never change them.

@nicm

nicm commented May 28, 2026

Copy link
Copy Markdown
Member

Once the timer expires, the intent is that they to be processed as keys. Otherwise if you typed (if you could do so quickly enough) or pasted a sequence, tmux would consume it. Whether this is useful or not is another thing, but that's the intent.

@chris-monardo

Copy link
Copy Markdown
Author

I've hit this regularly with slow SSH/terminal combos — the symptom is 20–30 garbage chars landing in the prompt on attach, which is hard to recover from without restarting tmux. After the 5 s timer, the trade-off is: a legitimate late DA response from the terminal (common in my experience) vs. a user pasting or typing a `\033[?…c` sequence (rare — DA-shaped sequences essentially don't occur in normal pasted content, and typing one inside the escape-time window is implausible). Both produce silent corruption in their respective failure modes, but the late-response case is the common one. Defaulting to it seems more useful, even with the acknowledged paste-corruption downside.

@nicm

nicm commented May 29, 2026

Copy link
Copy Markdown
Member

Do you regularly see it on terminals which are not Windows Terminal? Which ones? How slow is your connection? Can you show me a log?

@chris-monardo

Copy link
Copy Markdown
Author

Terminal:

echo $TERM
xterm-256color
image

Here are the relevant logs with annotations:

T=292.958862  server started (1881036): version 3.2a, socket /tmp/tmux-1385/daleak
T=293.599702  /dev/pts/51: keys are 10 (\033[>0;10;1c)
T=293.599751  /dev/pts/51: complete key \033[>0;10;1c 0xfe000000000   ← secondary DA consumed cleanly
              ... 5 s gap ...
              [tty_start_timer_callback fires at T≈297.958, ORs TTY_ALL_REQUEST_FLAGS into tty->flags]
T=298.556457  trigger script printf'd its passthrough payload
T=298.556528  input_dcs_dispatch: "tmux;\033[c\033[>c\033[>q"
T=298.556535  /dev/pts/51: \033[c\033[>c\033[>q                       ← three DA queries sent up
T=298.636700  keys are 16 (\033[?61;4;6;7;14;2)                       ← first part of DA response arrives
T=298.636723  complete key \033[ 0x10000000005b                       ← BYTE-LEVEL FALLBACK fires immediately
T=298.636740  keys are 14 (?61;4;6;7;14;2)
T=298.636751  complete key ? 0x3f                                     ← '?' dispatched as a literal key
              ... 14 more chars dispatched, one byte at a time ...
T=298.637890  input_parse_buffer: %0 ground, 17 bytes: ^[[?61;4;6;7;14;2
T=298.637897  screen_write_collect_end: 17 ^[[?61;4;6;7;14;2 (at 0,7)  ← LEAK lands in the pane
T=298.673444  keys are 33 (1;22;23;24;28;32;42;52c\033[>0;10;1c)
              ... 33 more byte-level fallbacks ...
T=298.675702  screen_write_collect_end: 34 1;22;23;24;28;32;42;52c^[[>0;10;1c (at 17,7)

This is actually just one of many byte leakage issues I've been facing. Other ones include bytes leaking any time I move my mouse over the pane, and click and dragging my mouse. Some of my sessions are getting completely corrupted such that my prefix command no longer works and I have to totally restart them multiple times a week.

I think its been happening much more often since I've been using claude, so something about that is causing the timing issues I think. I have noticed times where the pane tmux+claude appears completely unresponsive but my server and connection as a whole are responsive along with other tmux sessions.

@nicm

nicm commented Jun 4, 2026

Copy link
Copy Markdown
Member

This does not seem like the same problem as Windows Terminal where it is actually sending the responses twice.

It seems more like you have big problems with your network or computer or terminal or something. Making tmux ignore the sequences when they are late is not the right answer.

The fact there is a five second gap between query and response seems crazy.

You can try bumping TTY_QUERY_TIMEOUT in tty.c and for the broken mouse sequence bumping escape-time, but if I was you I would be trying to figure out what is happening to tmux in those five seconds.

@nicm

nicm commented Jun 4, 2026

Copy link
Copy Markdown
Member

Oh wait, I read what you are doing wrong. You are sending the queries with passthrough... what do you expect to happen? Why shouldn't tmux send them as keys, that's what you asked for?

@nicm nicm moved this from Not Started to Backlog in All Issues & PRs Jun 9, 2026
@nicm nicm moved this from Not Started to Waiting in All Issues & PRs Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting

Development

Successfully merging this pull request may close these issues.

Late DA response leaks to active pane after TTY_QUERY_TIMEOUT

3 participants