craft-cfb, craft-media: hostile-input hammer for CFB, ISO BMFF and Matroska (#1066)

The hammer over two compound files (version 3 and 4, and their first 2 KiB),
the synthetic ISO BMFF files of the fuzz test, and a synthetic Matroska file
plus four muxed ones (no lacing, Xiph, EBML, fixed-or-EBML), each opened and
read through the whole API. No panic found.

Ported from storytold/filmcraft#1794 (crates/cfb/src/tests/hostile.rs,
crates/isobmff/tests/fuzz.rs, crates/matroska/src/synthetic_tests/hostile.rs).

Co-authored-by: Logan Jennings <loganwjennings@gmail.com>
This commit is contained in:
Dylan M. TaylorandLogan Jennings authored and GitHub committed 2026-11-11 21:39:55 -05:00
1 parent 49dfd6ddc0
commit 06e949f30b
7 files changed
+123

No files matched your search

Generated
+3
View File
@@ -974,6 +974,9 @@ dependencies = [
[[package]]
name = "craft-cfb"
version = "0.1.0"
dependencies = [
"craft-media-testkit",
]
[[package]]
name = "craft-cms"
+3
View File
@@ -11,3 +11,6 @@ repository.workspace = true
workspace = true
[dependencies]
[dev-dependencies]
craft-media-testkit = { workspace = true }
+1
View File
@@ -3,6 +3,7 @@
use super::*;
mod hostile;
mod ported;
fn pattern(n: usize, seed: u8) -> Vec<u8> {
+30
View File
@@ -0,0 +1,30 @@
//! Hostile-input hammer: truncation, bit flips and extreme field values must never panic or hang.
use super::*;
fn exercise(d: &[u8]) {
let Ok(f) = CompoundFile::open(d) else { return };
let _ = f.root();
let _ = f.sector_size();
for id in 0..2000 {
if f.entry(id).is_none() {
break;
}
let _ = f.read(id);
let _ = f.child(id, "s0");
}
let _ = f.read(usize::MAX);
let _ = f.find("Header-2/Content-3b03/s4");
let _ = f.read_path("Mobs-19010/properties");
}
#[test]
fn hostile_input_never_panics() {
for (i, v) in [Version::V3, Version::V4].into_iter().enumerate() {
let (bytes, _) = sample(v, 5);
craft_media_testkit::hostile::hammer(&format!("cfb seed {i}"), &bytes, 15_000, exercise);
// The header and directory are where the structure lives: hammer a prefix harder.
let head = &bytes[..bytes.len().min(2048)];
craft_media_testkit::hostile::hammer(&format!("cfb head {i}"), head, 15_000, exercise);
}
}
@@ -10,6 +10,8 @@ use crate::matroska::mux::{el, el_float, el_str, el_uint};
use crate::matroska::track::ContentEncoding;
use crate::matroska::{ByteSource, Demuxer, MkvFile, OpenOptions, Packet, open_with};
mod hostile;
fn block(track: u8, rel: i16, flags: u8, data: &[u8]) -> Vec<u8> {
let mut b = vec![0x80 | track];
b.extend_from_slice(&rel.to_be_bytes());
@@ -0,0 +1,75 @@
//! Hostile-input hammer: truncation, bit flips and extreme field values must never panic or hang.
use super::*;
use crate::matroska::mux::{LacingMode, MkvWriter, MuxOptions, TrackSpec};
use crate::matroska::{MkvFile, TrackKind};
use std::io::Cursor;
fn muxed(lacing: LacingMode) -> Vec<u8> {
let mut v = TrackSpec::new(TrackKind::Video, "V_MJPEG");
v.video_size = Some((16, 16));
v.default_duration_ns = Some(40_000_000);
let mut a = TrackSpec::new(TrackKind::Audio, "A_PCM/INT/LIT");
a.audio = Some((48000.0, 1, Some(16)));
let mut s = TrackSpec::new(TrackKind::Subtitle, "S_TEXT/UTF8");
s.language = Some("fre".into());
let opts = MuxOptions { lacing, cluster_duration_ns: 100_000_000, ..Default::default() };
let mut m = MkvWriter::new(Cursor::new(Vec::new()), vec![v, a, s], opts).unwrap();
for i in 0..12i64 {
m.write_frame(0, i * 40_000_000, i % 4 == 0, &vec![i as u8; 30 + i as usize], None).unwrap();
for k in 0..3 {
m.write_frame(1, (i * 40 + k * 10) * 1_000_000, true, &[k as u8; 48], None).unwrap();
}
if i % 5 == 0 {
m.write_frame(2, i * 40_000_000, true, b"hello", Some(500_000_000)).unwrap();
}
}
m.finish().unwrap().into_inner()
}
fn walk(f: &MkvFile, d: &[u8]) {
for t in 0..f.tracks.len() {
let n = f.tracks[t].samples.len();
for k in [0, n / 2, n.saturating_sub(1), n, usize::MAX] {
let _ = f.read_sample(d, t, k);
let _ = f.read_sample_addition(d, t, k);
}
let _ = f.keyframe_before(t, i64::MIN);
let _ = f.keyframe_before(t, i64::MAX);
}
let _ = f.duration_ns();
}
fn exercise(d: &[u8]) {
for opts in
[OpenOptions { index: true, verify_crc: true, ..Default::default() }, OpenOptions { index: false, verify_crc: false, ..Default::default() }]
{
if let Ok(f) = open_with(d, &opts) {
walk(&f, d);
}
if let Ok(mut dm) = Demuxer::with_options(d, opts) {
let mut n = 0;
while let Ok(Some(_)) = dm.next_packet() {
n += 1;
if n > 100_000 {
break;
}
}
for t in 0..dm.tracks().len() {
let _ = dm.seek(t, 20_000_000);
let _ = dm.seek(t, i64::MAX);
let _ = dm.seek(t, i64::MIN);
let _ = dm.keyframe_index(t);
}
let _ = dm.build_index();
}
}
}
#[test]
fn hostile_input_never_panics() {
let seeds = [synthetic(), muxed(LacingMode::None), muxed(LacingMode::Xiph), muxed(LacingMode::Ebml), muxed(LacingMode::FixedOrEbml)];
for (i, s) in seeds.iter().enumerate() {
craft_media_testkit::hostile::hammer(&format!("matroska seed {i}"), s, 20_000, exercise);
}
}
+9
View File
@@ -175,6 +175,15 @@ fn mutated_ffmpeg_fixtures_never_panic() {
}
}
#[test]
fn hostile_hammer_never_panics() {
for (i, f) in synthetic_files().iter().enumerate() {
craft_media_testkit::hostile::hammer(&format!("isobmff seed {i}"), f, 20_000, |d| {
exercise(d);
});
}
}
#[test]
fn truncations_never_panic() {
for f in synthetic_files() {