From b9c397c47e2840be5a83f5e13e98ce02ceefa77a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:24:05 +0800 Subject: [PATCH] test(terminal): fuzz the four parsers a hostile file can reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parse_agent_event`, `osc::parse_notification`, `OscTokenizer` and the kitty graphics control parser all read bytes chosen by whatever runs in the pane — `cat` of a hostile file is enough. A panic in any of them is a pane a stranger can end, and osc.rs and cli_agent.rs had no fuzz coverage at all. Every truncation of eight seeds plus 4,000 seeded corruptions: no panic in any of them. That is a negative result, and it is only worth having because the harness was checked against planted panics in two of the four first — a fuzz that reaches nothing passes just as quietly as one that finds nothing. Read while writing it, and left alone because each is already right: `OscTokenizer` abandons a payload past MAX_PAYLOAD rather than truncating it, so an unterminated escape cannot grow the buffer without bound; the kitty parser returns None on `width * height * bpp` overflow and caps the inflate; and `parse_notification` indexes only what it has just measured. --- crates/tty7-core/src/core/cli_agent.rs | 78 ++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs index dfac1f04..64e600ea 100644 --- a/crates/tty7-core/src/core/cli_agent.rs +++ b/crates/tty7-core/src/core/cli_agent.rs @@ -1084,6 +1084,84 @@ mod tests { ); } + /// Nothing a program can write to a terminal makes a parser panic. + /// + /// These four read bytes chosen by whatever is running in the pane — + /// `cat` of a hostile file is enough — so a panic in any of them is a pane + /// a stranger can end. They are fuzzed together because they sit on the + /// same stream and share one property worth holding: an escape sequence is + /// either understood or ignored, never fatal. + /// + /// Seeded, so a failure reproduces from the round number in its message. + #[test] + fn no_terminal_escape_sequence_makes_a_parser_panic() { + use crate::core::kitty_graphics; + use crate::core::osc; + + let seeds: [&[u8]; 8] = [ + br#"777;notify;tty7://cli-agent;{"v":1,"agent":"claude","event":"turn-end"}"#, + b"777;notify;Build;done", + b"9;a plain notification", + b"9;4;progress", + b"Ga=T,f=100,s=2,v=2;iVBORw0KGgo=", + b"Ga=q,i=1,f=24,s=1,v=1,t=d;AAAA", + b"Ga=d,d=A,i=7", + b"", + ]; + + let feed = |bytes: &[u8]| { + // Each on its own: what matters is that none of them panics, not + // what any of them decides. + let _ = parse_agent_event(bytes); + let _ = osc::parse_notification(bytes); + let _ = kitty_graphics::Control::parse(bytes); + let _ = kitty_graphics::ImageDelete::decode(bytes); + // And through the tokenizer, which is what actually meets the + // stream: wrapped as a real OSC so it reaches the payload path. + let mut framed = b"\x1b]".to_vec(); + framed.extend_from_slice(bytes); + framed.extend_from_slice(b"\x07"); + let mut tok = osc::OscTokenizer::new(&[b"9", b"777", b"G"]); + tok.feed(&framed, |_| {}); + }; + + for seed in seeds { + feed(seed); + for cut in 0..=seed.len() { + feed(&seed[..cut]); + } + } + + let mut state = 0x9E37_79B9_7F4A_7C15u64; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + for round in 0..4_000 { + let seed = seeds[(next() as usize) % seeds.len()]; + let mut bytes = seed.to_vec(); + if bytes.is_empty() { + bytes.push(b'G'); + } + for _ in 0..(next() % 6 + 1) { + let at = (next() as usize) % bytes.len(); + match next() % 5 { + 0 => bytes[at] ^= 1 << (next() % 8), + 1 => bytes[at] = 0, + 2 => bytes[at] = b';', + 3 => bytes[at] = b'=', + _ => bytes.push((next() % 256) as u8), + } + } + // The round number is in scope for a failure message; a panic + // names this line and the seed above reproduces it. + let _ = round; + feed(&bytes); + } + } + #[test] fn parses_sentinel_events() { let ev = parse_agent_event(