diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index a568a8fa1..385e6a7d3 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,12 @@ wire format may change and such changes are recorded here. ## Unreleased +### A probe no longer panics on two adjacent start codes + +- `wai.video.avs3` and `wai.audio.avs3`: a payload with a start code prefix directly after another (`00 00 01 00 00 01`) + made the unit split slice backwards and panic, so a hostile payload could stop whatever probed it. It is now the empty + unit format-probes §8.2 reads, and the payload is refused. A test now gives every probe 30,000 payloads of hostile bytes. + ### A third party's hop has a signed record: what was sent, what came back, and what the provider bills - **The third-party hop record** ([extensions/route-record.md](extensions/route-record.md) §6.2, §7.2 step 9), diff --git a/wai/wai-rs/src/format_probe.rs b/wai/wai-rs/src/format_probe.rs index b04380cc1..dd7fc559f 100644 --- a/wai/wai-rs/src/format_probe.rs +++ b/wai/wai-rs/src/format_probe.rs @@ -164,6 +164,41 @@ mod tests { assert_eq!(probe(CAP_IMAGE_PNG, &[]), Err(Refusal::NoProbe)); } + /// A payload is whatever arrives: no probe may panic on it, whatever its bytes. + #[test] + fn no_bytes_make_a_probe_panic() { + let mut x = 0x9e37_79b9_7f4a_7c15u64; + let mut next = move || { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x + }; + // What the probes' framing is made of, so that noise reaches past their first checks. + const EDGE: [u8; 14] = [0, 0, 0, 1, 2, 3, 0x40, 0x42, 0x44, 0xb0, 0xb3, 0xff, 0x10, 0x7c]; + const STARTS: [&[u8]; 8] = [ + b"", b"\x00\x00\x01", b"\x00\x00\x01\x00\x00\x01", b"\x00\x00\x01\xb0", b"\x00\x00\x01\x40\x01", + b"\x00\x00\x01\x7c", b"\xff\x10\xff\x50", b"\xc0\x01\x10", + ]; + for round in 0..30_000u32 { + let mut b = STARTS[round as usize % STARTS.len()].to_vec(); + for _ in 0..next() as usize % 128 { + let r = next(); + b.push(if r & 1 == 0 { EDGE[(r >> 8) as usize % EDGE.len()] } else { (r >> 16) as u8 }); + } + for &(c, _) in PROBES { + let _ = probe(c, &b); + } + } + } + + #[test] + fn adjacent_start_codes_are_refused_and_do_not_panic() { + for c in [CAP_VIDEO_AVS3, CAP_AUDIO_AVS3, CAP_VIDEO_HEVC, CAP_VIDEO_VVC, CAP_VIDEO_LCEVC] { + assert!(probe(c, b"\x00\x00\x01\x00\x00\x01\xb0\x00\x00\x01").is_err(), "{c}"); + } + } + #[test] fn codes_are_distinct_and_read_back() { for r in Refusal::ALL { diff --git a/wai/wai-rs/src/format_probe/avs3.rs b/wai/wai-rs/src/format_probe/avs3.rs index e6086f259..614252d5a 100644 --- a/wai/wai-rs/src/format_probe/avs3.rs +++ b/wai/wai-rs/src/format_probe/avs3.rs @@ -37,7 +37,9 @@ pub fn units(b: &[u8]) -> Result, Refusal> { let mut out = Vec::new(); for (k, &s) in starts.iter().enumerate() { let end = if k + 1 < starts.len() { starts[k + 1] - 3 } else { b.len() }; - out.push((b[s], &b[s + 1..end])); + // A prefix directly after a prefix: that unit's start code is the next prefix's first + // byte, and it holds nothing. + out.push((b[s], b.get(s + 1..end).unwrap_or(&[]))); } Ok(out) }