diff --git a/rust/core/src/clients/irc.rs b/rust/core/src/clients/irc.rs index 3b00ab5..7d6d15b 100644 --- a/rust/core/src/clients/irc.rs +++ b/rust/core/src/clients/irc.rs @@ -1052,6 +1052,23 @@ mod tests { use std::io::{BufRead, BufReader}; use std::net::TcpListener; + #[test] + fn hostile_lines_never_panic() { + let client = Client::new(Config::default()); + client.st().nick = "me".into(); + client.handle_message(&parse_line(":me!u@h JOIN #a").unwrap()); + let mut n = crate::Noise::new(0x1bc); + for _ in 0..5000 { + let raw = n.bytes(120, b":!@# ,;=\x01\x02\x03\x0f\x16\x1d\x1fme#aJOINPARTKICKPRIVMSGNOTICE3534330011234567890"); + let line = String::from_utf8_lossy(&raw); + if let Some(m) = parse_line(&line) { + client.handle_message(&m); + let e = Event { kind: "message".into(), target: m.param(0).into(), from: m.source.clone(), text: m.last().into(), ..Default::default() }; + let _ = crate::ircformat::format_event(&e, "12:00"); + } + } + } + #[test] fn remember_room_membership_tracks_confirmed_channels() { let updates = Arc::new(Mutex::new(Vec::new())); diff --git a/rust/core/src/clients/telnet.rs b/rust/core/src/clients/telnet.rs index 0271491..50cc6a3 100644 --- a/rust/core/src/clients/telnet.rs +++ b/rust/core/src/clients/telnet.rs @@ -322,6 +322,19 @@ mod tests { use std::sync::mpsc; use std::time::Duration; + #[test] + fn hostile_negotiation_never_panics() { + let c = TelnetClient::new(&Profile::new(crate::models::Protocol::Telnet), Callbacks::noop(), None); + let mut n = crate::Noise::new(0x7e1); + let mut pending = Vec::new(); + for _ in 0..5000 { + // IAC, SE, SB, WILL, WONT, DO, DONT and the options the client handles. + let chunk = n.bytes(64, &[255, 240, 250, 251, 252, 253, 254, 1, 3, 24, 31, 32, 0, b'a']); + c.inner.process(&mut pending, &chunk); + assert!(pending.len() <= 16 << 10, "pending must stay bounded"); + } + } + #[test] fn line_endings() { let cases = [ diff --git a/rust/core/src/clients/vnc.rs b/rust/core/src/clients/vnc.rs index 8d1c1e7..1cb157a 100644 --- a/rust/core/src/clients/vnc.rs +++ b/rust/core/src/clients/vnc.rs @@ -866,6 +866,44 @@ mod tests { use super::*; use std::net::TcpListener; + #[test] + fn hostile_updates_never_panic() { + use flate2::{Compress, Compression, FlushCompress}; + let mut n = crate::Noise::new(0x5eed); + let (mut fb, mut dec) = (Framebuffer::new(32, 32), Decoders::default()); + let mut z = Compress::new(Compression::fast(), true); + let mut decoded = [0usize; 2]; // successful ZRLE / other updates + for _ in 0..3000 { + let enc: i32 = [0, 1, 5, 7, 16, -223, -224, -308, 99][n.below(9)]; + let dim = |n: &mut crate::Noise| n.below(40) as u16; + let (x, y, w, h) = (dim(&mut n), dim(&mut n), dim(&mut n), dim(&mut n)); + let mut msg = vec![0, 0, 1]; + for v in [x, y, w, h] { + msg.extend_from_slice(&v.to_be_bytes()); + } + msg.extend_from_slice(&enc.to_be_bytes()); + let body = n.bytes(600, &[0, 1, 2, 0xff, 0x80, 0x10, 0x1f]); + if enc == 16 { + // ZRLE: random tiles behind a real zlib stream, so the decoder itself is reached. + let mut out = Vec::with_capacity(body.len() * 2 + 64); + z.compress_vec(&body, &mut out, FlushCompress::Sync).unwrap(); + msg.extend_from_slice(&(out.len() as u32).to_be_bytes()); + msg.extend_from_slice(&out); + } else { + msg.extend_from_slice(&body); + } + match read_update(&mut &msg[..], &mut fb, &mut dec) { + Ok(_) => decoded[(enc != 16) as usize] += 1, + // The persistent zlib streams may have desynchronised; start them afresh. + Err(_) => (dec, z) = (Decoders::default(), Compress::new(Compression::fast(), true)), + } + if fb.width as usize * fb.height as usize > 1 << 20 { + fb = Framebuffer::new(32, 32); + } + } + assert!(decoded.iter().all(|&d| d > 0), "the harness must reach the decoders: {decoded:?}"); + } + #[test] fn des_response_matches_known_vector() { // openssl enc -des-ecb -K 0e86ceceeef64e26 (bit-reversed "password") over "0123456789abcdef". diff --git a/rust/core/src/lib.rs b/rust/core/src/lib.rs index 2a4ec6e..1220562 100644 --- a/rust/core/src/lib.rs +++ b/rust/core/src/lib.rs @@ -19,3 +19,34 @@ pub mod validation; pub type Error = Box; pub type Result = std::result::Result; + +/// Seeded pseudo-random input for the parsers' "hostile bytes never panic" tests. Not coverage +/// guided like cargo-fuzz, but runs on stable in every CI run. +#[cfg(test)] +pub(crate) struct Noise(u64); + +#[cfg(test)] +impl Noise { + pub fn new(seed: u64) -> Noise { + Noise(seed | 1) + } + + pub fn next(&mut self) -> u64 { + // xorshift64 + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + pub fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + + /// Up to `max` bytes, mostly drawn from `alphabet` (the bytes a parser cares about) and + /// otherwise uniformly random. + pub fn bytes(&mut self, max: usize, alphabet: &[u8]) -> Vec { + let n = self.below(max + 1); + (0..n).map(|_| if self.below(3) > 0 { alphabet[self.below(alphabet.len())] } else { self.next() as u8 }).collect() + } +} diff --git a/rust/core/src/sshconfig.rs b/rust/core/src/sshconfig.rs index 3fda6ff..859eef7 100644 --- a/rust/core/src/sshconfig.rs +++ b/rust/core/src/sshconfig.rs @@ -175,6 +175,23 @@ fn is_trueish(v: &str) -> bool { mod tests { use super::*; + #[test] + fn hostile_configs_never_panic() { + let mut n = crate::Noise::new(0x55c); + let words: &[&[u8]] = + &[b"Host ", b"HostName ", b"Port ", b"User ", b"ProxyJump ", b"IdentityFile ", b"Match ", b"*", b"=", b"\n", b"\"", b" ", b"a@b:22,", b"99999"]; + for _ in 0..3000 { + let mut text = Vec::new(); + for _ in 0..n.below(40) { + text.extend_from_slice(words[n.below(words.len())]); + } + text.extend(n.bytes(20, b" \t\n=")); + if let Ok(blocks) = parse(&String::from_utf8_lossy(&text)) { + let _ = to_profiles(&blocks, "f"); + } + } + } + const TEST_CONFIG: &str = "# Test SSH config Host * diff --git a/rust/core/src/updater.rs b/rust/core/src/updater.rs index 09f95cf..f2fb0ce 100644 --- a/rust/core/src/updater.rs +++ b/rust/core/src/updater.rs @@ -145,6 +145,15 @@ fn leading_int(s: Option<&&str>) -> i64 { #[cfg(test)] mod tests { use super::*; + + #[test] + fn hostile_manifests_never_panic() { + let mut n = crate::Noise::new(0x9d); + for _ in 0..5000 { + let body = n.bytes(200, br#"{}[]":,0123456789.vlatestVersionreleaseURL"#); + let _ = parse_manifest(&body, "1.2.3"); + } + } use std::io::Write; /// Serves one HTTP response on a random local port; returns its URL.