use std::cell::RefCell;
use std::io::Cursor;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Once;
use rff::Engine;
use rff_core::Packet;
fn xs(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
fn gen_input(mut s: u64, starters: &[&[u8]]) -> Vec<u8> {
s |= 1;
let base = starters[(xs(&mut s) as usize) % starters.len()];
let mut buf = base.to_vec();
let nmut = 1 + xs(&mut s) % 24;
for _ in 0..nmut {
if buf.is_empty() {
buf.push(xs(&mut s) as u8);
}
match xs(&mut s) % 6 {
0 => {
let i = xs(&mut s) as usize % buf.len();
buf[i] ^= xs(&mut s) as u8;
}
1 => {
let i = xs(&mut s) as usize % (buf.len() + 1);
buf.insert(i.min(buf.len()), xs(&mut s) as u8);
}
2 if buf.len() > 1 => {
let i = xs(&mut s) as usize % buf.len();
buf.remove(i);
}
3 => {
let i = xs(&mut s) as usize % (buf.len() + 1);
let v = (xs(&mut s) & 0x00FF_FFFF) as u32;
for b in v.to_be_bytes() {
buf.insert(i.min(buf.len()), b);
}
}
4 => {
let n = xs(&mut s) as usize % (buf.len() + 1);
buf.truncate(n);
}
_ => {
let i = xs(&mut s) as usize % buf.len();
buf[i] = xs(&mut s) as u8;
}
}
if buf.len() > 1 << 16 {
buf.truncate(1 << 16);
}
}
buf
}
fn container_starters() -> Vec<&'static [u8]> {
vec![
b"RIFF\x00\x10\x00\x00AVI LIST",
b"RIFF\x00\x10\x00\x00WAVEfmt ",
b"\x1aE\xdf\xa3\x01\x00\x00\x00", b"\x00\x00\x00\x18ftypmp42",
b"\x00\x00\x00\x1cftypisom",
b"OggS\x00\x02\x00\x00\x00\x00\x00\x00",
b"\x47\x40\x00\x10\x00\x00\xb0\x0d", b"FLV\x01\x05\x00\x00\x00\x09",
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR",
b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01",
b"GIF89a\x10\x00\x10\x00\x80\x00\x00",
b"fLaC\x00\x00\x00\x22",
b"RIFF\x00\x10\x00\x00WEBPVP8 ",
b"WEBVTT\n\n00:00:00.000 --> 00:00:01.000\n",
b"1\n00:00:01,000 --> 00:00:02,000\n",
b"\x00\x00\x00\x00", ]
}
fn codec_starters() -> Vec<&'static [u8]> {
vec![
b"\x00\x00\x00\x01\x67", b"\x00\x00\x01\x09",
b"\xff\xfb\x90\x00", b"\xff\xf3\x80\x00", b"\xff\xf1\x50\x80", b"\x82\x49\x83\x42", b"\x83\x00\x00\x00",
b"OpusHead\x01\x02",
b"\x1f\x8b\x08\x00", b"\x00\x00\x00\x00\x00\x00\x00", b"\xff\xff\xff\xff",
b"\x30\x01\x02\x03",
]
}
fn install_panic_capture() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
std::panic::set_hook(Box::new(|info| {
let loc = info
.location()
.map(|l| format!("{}:{}", l.file(), l.line()))
.unwrap_or_else(|| "<unknown>".into());
let msg = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic>".into()
};
LAST_PANIC.with(|c| *c.borrow_mut() = Some(format!("{loc} :: {msg}")));
}));
});
}
thread_local! {
static LAST_PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
}
struct Finding {
site: String, target: String, seed: u64, hex: String, }
fn iters() -> u64 {
std::env::var("RFF_FUZZ_ITERS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(400)
}
fn run_and_capture<F: FnOnce()>(f: F) -> Option<String> {
LAST_PANIC.with(|c| *c.borrow_mut() = None);
let r = catch_unwind(AssertUnwindSafe(f));
if r.is_err() {
Some(
LAST_PANIC
.with(|c| c.borrow_mut().take())
.unwrap_or_else(|| "<no location>".into()),
)
} else {
None
}
}
fn report(findings: Vec<Finding>) {
if findings.is_empty() {
return;
}
let mut seen = std::collections::BTreeMap::<String, &Finding>::new();
for f in &findings {
seen.entry(f.site.clone()).or_insert(f);
}
let mut msg = format!(
"\n{} distinct panic site(s) across the untrusted-input fuzz sweep:\n",
seen.len()
);
for (site, f) in &seen {
msg += &format!(
" • {site}\n target={} seed={} input={}…\n",
f.target, f.seed, f.hex
);
}
panic!("{msg}");
}
#[test]
fn demuxers_never_panic_fuzz() {
install_panic_capture();
let engine = Engine::new();
let starters = container_starters();
let mut names: Vec<&'static str> = engine
.formats
.iter()
.filter(|f| f.can_demux())
.map(|f| f.name)
.collect();
names.sort_unstable();
let mut findings = Vec::new();
let mut execs = 0u64;
let ntargets = names.len();
for name in names {
let mut base = 0x9E37_79B9_7F4A_7C15u64 ^ hash(name);
for it in 0..iters() {
execs += 1;
let seed = base.wrapping_add(it).wrapping_mul(0x2545_F491_4F6C_DD1D);
let input = gen_input(seed, &starters);
let inp = input.clone();
if let Some(site) = run_and_capture(|| {
if let Ok(mut d) = engine
.formats
.open_demuxer(name, Box::new(Cursor::new(inp)))
{
let _ = d.read_header();
for _ in 0..64 {
if d.read_packet().is_err() {
break;
}
}
}
}) {
findings.push(Finding {
site,
target: name.into(),
seed,
hex: hexlead(&input),
});
}
}
base = base.wrapping_add(1);
let _ = base;
}
eprintln!(
"demux fuzz: {ntargets} demuxers × {} iters = {execs} execs",
iters()
);
report(findings);
}
#[test]
fn decoders_never_panic_fuzz() {
install_panic_capture();
let engine = Engine::new();
let starters = codec_starters();
let mut names: Vec<&'static str> = engine
.codecs
.iter()
.filter(|c| c.can_decode())
.map(|c| c.name)
.collect();
names.sort_unstable();
let external_aborts: &[&str] = if cfg!(debug_assertions) {
&["avif"]
} else {
&[]
};
let mut findings = Vec::new();
for name in names {
if external_aborts.contains(&name) {
continue;
}
let Some(codec) = engine.codecs.by_name(name) else {
continue;
};
for it in 0..iters() {
let seed = (0xD1B5_4A32_D192_ED03u64 ^ hash(name))
.wrapping_add(it)
.wrapping_mul(0x2545_F491_4F6C_DD1D);
let input = gen_input(seed, &starters);
let inp = input.clone();
let id = codec.id;
if let Some(site) = run_and_capture(|| {
if let Ok(mut dec) = engine.codecs.find_decoder(id) {
let _ = dec.send_packet(&Packet::from_data(0, inp));
for _ in 0..64 {
if dec.receive_frame().is_err() {
break;
}
}
}
}) {
findings.push(Finding {
site,
target: name.into(),
seed,
hex: hexlead(&input),
});
}
}
}
report(findings);
}
fn hash(s: &str) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01B3);
}
h
}
fn hexlead(b: &[u8]) -> String {
b.iter().take(64).map(|x| format!("{x:02x}")).collect()
}