AV1 limits are the level 6.x ones (16384 wide, 8704 high, 35651584 samples); hang guard instead of per-run time asserts; encoder failure is not a skip

- decoder.rs: MaxHSize 16384, MaxVSize 8704 and MaxPicSize 35651584 per AV1 Annex A (the earlier
  16384 x 8704 product is four times the area limit); boundary unit tests on frame_size_allowed;
  README states the three limits.
- testkit: with_deadline/set_progress (whole-sweep hang guard naming the last input;
  FILMCRAFT_HOSTILE_TIMEOUT_SECS overrides) and ffmpeg_has_encoder.
- The nine fuzz_hostile tests drop their per-run elapsed assertions (which could not catch a
  hang and could fail a slow runner) and run under the guard.
- A seed whose encoder is absent from ffmpeg still skips (and fails under
  FILMCRAFT_REQUIRE_ORACLES=1); an encoder that is present but fails now fails the test.
- mpeg2v failure labels carry the seed name.
This commit is contained in:
Logan Jennings
2026-10-19 19:20:37 -07:00
parent 0fd76d55e8
commit c1e431ad2d
15 changed files with 225 additions and 64 deletions
+19 -11
View File
@@ -5,7 +5,6 @@
mod common;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::time::{Duration, Instant};
use filmcraft_mpeg2v::{Decoder, access_units, probe, scan_access_unit};
@@ -60,9 +59,9 @@ fn count_panics() {
}
fn run_all(d: &[u8], label: &str) {
filmcraft_testkit::set_progress(label);
count_panics();
let before = PANICS.load(std::sync::atomic::Ordering::SeqCst);
let t = Instant::now();
let res = catch_unwind(AssertUnwindSafe(|| {
let _ = probe(d);
let _ = scan_access_unit(d);
@@ -78,38 +77,47 @@ fn run_all(d: &[u8], label: &str) {
}
}));
assert!(res.is_ok() && PANICS.load(std::sync::atomic::Ordering::SeqCst) == before, "panic: {label}");
assert!(t.elapsed() < Duration::from_secs(20), "too slow ({:?}): {label}", t.elapsed());
}
/// Seconds the whole sweep may take before it counts as a hang (override: FILMCRAFT_HOSTILE_TIMEOUT_SECS).
const DEADLINE_SECS: u64 = 600;
#[test]
fn hostile_streams_never_panic() {
filmcraft_testkit::with_deadline(DEADLINE_SECS, sweep);
}
fn sweep() {
let Some(ff) = filmcraft_testkit::ffmpeg_or_skip("mpeg2v fuzz") else { return };
for spec in &SPECS {
let Some(path) = common::make(&ff, spec) else {
filmcraft_testkit::skip("mpeg2v fuzz", &format!("ffmpeg could not encode the {} seed (encoder missing?)", spec.name));
if !filmcraft_testkit::ffmpeg_has_encoder(&ff, "mpeg2video") {
filmcraft_testkit::skip("mpeg2v fuzz", &format!("this ffmpeg has no mpeg2video encoder (seed {})", spec.name));
continue;
}
let Some(path) = common::make(&ff, spec) else {
panic!("ffmpeg has mpeg2video but could not encode the {} seed", spec.name);
};
fuzz(&std::fs::read(path).unwrap());
fuzz(spec.name, &std::fs::read(path).unwrap());
}
}
fn fuzz(seed: &[u8]) {
fn fuzz(name: &str, seed: &[u8]) {
// every truncation length (stride keeps it quick on larger seeds)
let stride = (seed.len() / 600).max(1);
for n in (0..seed.len()).step_by(stride) {
run_all(&seed[..n], &format!("truncate {n}"));
run_all(&seed[..n], &format!("{name} truncate {n}"));
}
// header region: every byte to 0 / 0xff / 0x80 / each single bit
for p in 0..seed.len().min(96) {
for v in [0u8, 0xff, 0x80, 0x01] {
let mut d = seed.to_vec();
d[p] = v;
run_all(&d, &format!("byte {p}={v:#x}"));
run_all(&d, &format!("{name} byte {p}={v:#x}"));
}
for b in 0..8 {
let mut d = seed.to_vec();
d[p] ^= 1 << b;
run_all(&d, &format!("flip {p}.{b}"));
run_all(&d, &format!("{name} flip {p}.{b}"));
}
}
// random multi-mutations
@@ -124,7 +132,7 @@ fn fuzz(seed: &[u8]) {
d[p] = rng.next() as u8;
}
}
run_all(&d, &format!("random {i}"));
run_all(&d, &format!("{name} random {i}"));
}
}