Skip to content

Unrecognised CSI ? … reports wedge the parser and swallow all later input #1104

Description

@Junyi-99

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions