Files
filmcraft/crates/vp8/examples/fuzz.rs
MrCloudy2andClaude Opus 5.5 53a0d959fe #432: VP8 video decodes (clean-room pure-Rust VP8 decoder)
VP8 WebM showed black video: there was no VP8 decoder. New L0 crate
filmcraft-vp8, implemented from RFC 6386 sections 1-19 (tables extracted
from the RFC text, nothing from the attached reference decoder): every
coding tool, versions 0-3, hidden altref frames, size changes. Bit-exact
with libvpx on 62 libvpx VP8 test vectors, real-world FATE samples and
generated fixtures; never panics on corrupted input.

Wired into codecs for Matroska / WebM (V_VP8, including the alpha layer
of transparent WebM) and MP4 (vp08). VP8 colour is BT.601 (RFC 6386 9.2).
A hidden altref frame is a WebM block of its own with the time of the
frame that follows it: the VP8 time lookup now moves on to that shown
frame instead of repeating the previous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:13:07 +02:00

101 lines
3.8 KiB
Rust

//! Decode corrupted variants of IVF files and report panics (`fuzz iterations seed files...`).
//! Run in a debug build so arithmetic overflow panics too.
fn frames(data: &[u8]) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let Some(h) = data.get(6..8) else { return out };
let mut pos = usize::from(u16::from_le_bytes([h[0], h[1]]));
while pos + 12 <= data.len() {
let size = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 12;
out.push(data[pos..(pos + size).min(data.len())].to_vec());
pos += size;
}
out
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let iterations: usize = args[1].parse().unwrap_or(100);
let mut seed: u64 = args[2].parse().unwrap_or(1);
let mut rnd = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
std::panic::set_hook(Box::new(|_| {}));
let mut panics = 0;
let mut decoded = 0u64;
for path in &args[3..] {
let original = frames(&std::fs::read(path).unwrap_or_default());
for it in 0..iterations {
let mut fr = original.clone();
let mode = rnd() % 6;
for f in fr.iter_mut() {
if f.is_empty() {
continue;
}
match mode {
0 => {
// a few bit flips per frame
for _ in 0..1 + rnd() % 4 {
let i = (rnd() as usize) % f.len();
f[i] ^= 1 << (rnd() % 8);
}
}
1 => {
let n = (rnd() as usize) % (f.len() + 1);
f.truncate(n);
}
2 => {
// random bytes in the header region (frame tag, sizes, first partition)
for _ in 0..1 + rnd() % 3 {
let i = (rnd() as usize) % f.len().min(24);
f[i] = rnd() as u8;
}
}
3 => {
for b in f.iter_mut().skip(10) {
if rnd() % 50 == 0 {
*b = rnd() as u8;
}
}
}
// trailing garbage on some frames
4 if rnd() % 3 == 0 => f.extend((0..rnd() % 64).map(|_| rnd() as u8)),
_ => {}
}
}
if mode == 5 {
// decode order shuffled: inter frames after the wrong references
for i in (1..fr.len()).rev() {
let j = (rnd() as usize) % (i + 1);
fr.swap(i, j);
}
}
let r = std::panic::catch_unwind(|| {
let mut dec = filmcraft_vp8::Decoder::new();
let mut n = 0u64;
for (k, f) in fr.iter().enumerate() {
let _ = dec.is_disposable(f);
if dec.decode(f, k as i64).is_ok() {
n += 1;
}
let _ = filmcraft_vp8::keyframe_info(f);
}
n
});
match r {
Ok(n) => decoded += n,
Err(e) => {
panics += 1;
let msg = e.downcast_ref::<String>().cloned().or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default();
eprintln!("PANIC {path} iteration {it} mode {mode}: {msg}");
}
}
}
}
eprintln!("done: {panics} panics, {decoded} frames decoded");
}