clippy 1.99.1, warnings denied, every target. Nested ifs joined, is_multiple_of, as_chunks, slice::from_ref, a lifetime elided, private long types named, an unused binding and an unused import removed. Nat::limbs is read by sink_energy alone and is allowed unused without it. select_entries, float_parts and cmcd's members keep their public signatures spelled out, allowed with the reason; film's receipt tests are not the module's last item. The C2PA emitter's tests read a signed asset through Reader::from_context(..).with_stream(..), which the SDK names in place of the deprecated Reader::from_stream. No behaviour changes. Co-Authored-By: Claude <[email protected]>
134 lines
6.1 KiB
Rust
134 lines
6.1 KiB
Rust
//! A container's reader reserves no more entries than its bytes could hold.
|
|
//!
|
|
//! Each extension container (world, audio scene, reel, score, haptic, splat4d) reads a
|
|
//! `u32` count of entries from its input and used to reserve that many before reading
|
|
//! one: a few bytes could ask the allocator for gigabytes. The coverage-guided fuzzer
|
|
//! (`fuzz/`, target `containers`) found it in the audio scene's object table, asking
|
|
//! for 48 GiB.
|
|
//!
|
|
//! For each container a committed vector is rewritten so that the section holding a
|
|
//! count declares 2^32 - 1 entries and holds nothing after the count. The reader must
|
|
//! refuse it having asked for no single allocation past a few megabytes. The allocator
|
|
//! here records the largest request, and refuses any past 256 MiB, so a reader that
|
|
//! still sizes its reservation from the count fails here instead of reserving.
|
|
#![cfg(all(
|
|
feature = "world",
|
|
feature = "audio_scene",
|
|
feature = "film",
|
|
feature = "score",
|
|
feature = "haptic",
|
|
feature = "splat4d"
|
|
))]
|
|
|
|
use std::alloc::{GlobalAlloc, Layout, System};
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
|
|
|
|
/// The system allocator, recording the largest single request.
|
|
struct Largest;
|
|
static LARGEST: AtomicUsize = AtomicUsize::new(0);
|
|
const REFUSE_PAST: usize = 256 << 20;
|
|
|
|
unsafe impl GlobalAlloc for Largest {
|
|
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
|
|
LARGEST.fetch_max(l.size(), Relaxed);
|
|
if l.size() > REFUSE_PAST { std::ptr::null_mut() } else { unsafe { System.alloc(l) } }
|
|
}
|
|
unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 {
|
|
LARGEST.fetch_max(l.size(), Relaxed);
|
|
if l.size() > REFUSE_PAST { std::ptr::null_mut() } else { unsafe { System.alloc_zeroed(l) } }
|
|
}
|
|
unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 {
|
|
LARGEST.fetch_max(new, Relaxed);
|
|
if new > REFUSE_PAST { std::ptr::null_mut() } else { unsafe { System.realloc(p, l, new) } }
|
|
}
|
|
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
|
|
unsafe { System.dealloc(p, l) }
|
|
}
|
|
}
|
|
|
|
#[global_allocator]
|
|
static A: Largest = Largest;
|
|
|
|
fn vector(rel: &str) -> Vec<u8> {
|
|
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join(rel);
|
|
std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
|
|
}
|
|
|
|
/// The sections of a container in the layout every one of these shares: a magic, a
|
|
/// `u16` count, a table of `(kind u8, offset u32, length u32)`, then the bodies.
|
|
fn sections(b: &[u8]) -> Vec<(u8, Vec<u8>)> {
|
|
let n = u16::from_le_bytes([b[4], b[5]]) as usize;
|
|
let body = &b[6 + 9 * n..];
|
|
(0..n)
|
|
.map(|i| {
|
|
let e = &b[6 + 9 * i..6 + 9 * (i + 1)];
|
|
let off = u32::from_le_bytes(e[1..5].try_into().unwrap()) as usize;
|
|
let len = u32::from_le_bytes(e[5..9].try_into().unwrap()) as usize;
|
|
(e[0], body[off..off + len].to_vec())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The container rebuilt with section `kind`'s body replaced by a count of 2^32 - 1.
|
|
fn declaring_too_many(b: &[u8], kind: u8) -> Vec<u8> {
|
|
let mut secs = sections(b);
|
|
let s = secs.iter_mut().find(|(k, _)| *k == kind).expect("the vector has the section");
|
|
s.1 = u32::MAX.to_le_bytes().to_vec();
|
|
let mut out = b[..4].to_vec();
|
|
out.extend_from_slice(&(secs.len() as u16).to_le_bytes());
|
|
let mut off = 0u32;
|
|
for (k, body) in &secs {
|
|
out.push(*k);
|
|
out.extend_from_slice(&off.to_le_bytes());
|
|
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
|
|
off += body.len().div_ceil(8) as u32 * 8;
|
|
}
|
|
for (_, body) in &secs {
|
|
out.extend_from_slice(body);
|
|
out.resize(out.len() + (body.len().div_ceil(8) * 8 - body.len()), 0);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn each_container_refuses_a_count_its_bytes_cannot_hold_without_reserving_it() {
|
|
type Case = (&'static str, &'static str, u8, fn(&[u8]) -> bool);
|
|
let cases: [Case; 7] = [
|
|
("world assets", "wai-web/demo/vectors/gallery.wwld", 0x03, |b| wai::world::WorldContainer::from_bytes(b).is_err()),
|
|
("audio scene objects", "wai-web/demo/audio/moving.wscn", 0x02, |b| wai::audio_scene::SpatialScene::from_bytes(b).is_err()),
|
|
("audio scene assets", "wai-web/demo/audio/moving.wscn", 0x03, |b| wai::audio_scene::SpatialScene::from_bytes(b).is_err()),
|
|
("reel shots", "film-conformance/vectors/coverage.wrel", 0x03, |b| wai::film::Reel::from_bytes(b).is_err()),
|
|
("score notes", "score-conformance/vectors/arp.wscr", 0x02, |b| wai::score::Score::from_bytes(b).is_err()),
|
|
("haptic channels", "haptic-conformance/vectors/ramp.whap", 0x02, |b| wai::haptic::HapticSignal::from_bytes(b).is_err()),
|
|
("splat4d tracks", "volumetric-conformance/vectors/morph.w4ds", 0x02, |b| wai::splat4d::Splat4dClipData::from_bytes(b).is_err()),
|
|
];
|
|
for (what, rel, kind, refused) in cases {
|
|
let original = vector(rel);
|
|
let bytes = if sections(&original).iter().any(|(k, _)| *k == kind) {
|
|
declaring_too_many(&original, kind)
|
|
} else {
|
|
// The vector carries no such section (the audio scene's assets are optional):
|
|
// add one, as the last section.
|
|
let mut with = original.clone();
|
|
let n = u16::from_le_bytes([with[4], with[5]]);
|
|
with[4..6].copy_from_slice(&(n + 1).to_le_bytes());
|
|
let secs = sections(&original);
|
|
let end: usize = secs.iter().map(|(_, b)| b.len().div_ceil(8) * 8).sum();
|
|
let mut entry = vec![kind];
|
|
entry.extend_from_slice(&(end as u32).to_le_bytes());
|
|
entry.extend_from_slice(&4u32.to_le_bytes());
|
|
let table_end = 6 + 9 * n as usize;
|
|
with.splice(table_end..table_end, entry);
|
|
let body_start = table_end + 9;
|
|
with.truncate(body_start + end);
|
|
with.resize(body_start + end, 0);
|
|
with.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
with
|
|
};
|
|
LARGEST.store(0, Relaxed);
|
|
assert!(refused(&bytes), "{what}: 2^32 - 1 entries in four bytes were accepted");
|
|
let largest = LARGEST.load(Relaxed);
|
|
assert!(largest <= 8 << 20, "{what}: the reader asked for {largest} bytes at once");
|
|
}
|
|
}
|