Summary
When the terminal sends a DEC private-mode report that crossterm has no parser for (any CSI ? … <final> other than ?…u or ?…c), the parser buffer is never cleared. Every byte that arrives afterwards is appended to it, so all subsequent user input is silently discarded until a stray u or c happens to error the buffer out.
Cause
parse_csi (src/event/sys/unix/parse.rs):
b'?' => match buffer[buffer.len() - 1] {
b'u' => return parse_csi_keyboard_enhancement_flags(buffer),
b'c' => return parse_csi_primary_device_attributes(buffer),
_ => None, // <-- becomes Ok(None)
},
Ok(None) means "incomplete, keep reading", and the reader acts on that (src/event/source/unix/mio.rs):
Ok(None) => {
// Event can't be parsed, because we don't have enough bytes for
// the current sequence. Keep the buffer and process next bytes.
}
But the sequence is not incomplete — a final byte in 0x40..=0x7E already terminated it. No later byte can make it parse, so the buffer grows forever and swallows everything appended to it.
Reproduction
// A DECRPM report, e.g. the reply to `CSI ? 2026 $ p` (synchronized output).
assert!(parse_event(b"\x1B[?2026;2$y", false).is_err()); // fails: returns Ok(None)
End to end: enable DEC private mode 2031 (color-scheme change notifications, supported by Ghostty, kitty, Contour, foot, VTE, and forwarded by tmux 3.6+), then change the desktop between light and dark. The terminal sends CSI ? 997 ; 1 n, and from that moment read() returns nothing for anything the user types.
Any unrecognised DEC private report reaches this: DECRPM replies ($y), CSI ? 997 n, and so on.
Suggested fix
Treat a finished-but-unrecognised sequence as unparseable rather than incomplete, so the reader resets its buffer:
b'?' => match buffer[buffer.len() - 1] {
b'u' => return parse_csi_keyboard_enhancement_flags(buffer),
b'c' => return parse_csi_primary_device_attributes(buffer),
// A final byte we have no parser for still *ends* the sequence.
0x40..=0x7E => return Err(could_not_parse_event_error()),
_ => None,
},
The report is then dropped (which is the current intent) instead of poisoning the buffer.
I have this running on a fork along with a CSI ? 997 parser; happy to open a PR with just the fix above, or with both, whichever you prefer.
Environment
- crossterm 0.29.0
- macOS 27.0, tmux 3.7b
- Reproduces on any terminal that emits an unrecognised DEC private report; the parser unit test above needs no terminal at all.
Summary
When the terminal sends a DEC private-mode report that crossterm has no parser for (any
CSI ? … <final>other than?…uor?…c), the parser buffer is never cleared. Every byte that arrives afterwards is appended to it, so all subsequent user input is silently discarded until a strayuorchappens to error the buffer out.Cause
parse_csi(src/event/sys/unix/parse.rs):Ok(None)means "incomplete, keep reading", and the reader acts on that (src/event/source/unix/mio.rs):But the sequence is not incomplete — a final byte in
0x40..=0x7Ealready terminated it. No later byte can make it parse, so the buffer grows forever and swallows everything appended to it.Reproduction
End to end: enable DEC private mode 2031 (color-scheme change notifications, supported by Ghostty, kitty, Contour, foot, VTE, and forwarded by tmux 3.6+), then change the desktop between light and dark. The terminal sends
CSI ? 997 ; 1 n, and from that momentread()returns nothing for anything the user types.Any unrecognised DEC private report reaches this: DECRPM replies (
$y),CSI ? 997 n, and so on.Suggested fix
Treat a finished-but-unrecognised sequence as unparseable rather than incomplete, so the reader resets its buffer:
The report is then dropped (which is the current intent) instead of poisoning the buffer.
I have this running on a fork along with a
CSI ? 997parser; happy to open a PR with just the fix above, or with both, whichever you prefer.Environment