Some checks failed
workflow hygiene / every workflow supersedes its own queue (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Has been cancelled
WAI section references / cited sections exist (pull_request) Has been cancelled
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Has been cancelled
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Has been cancelled
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Has been cancelled
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been cancelled
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
tests in CI / every test file is run by a job or listed with its reason (pull_request) Has been cancelled
standards workspaces build + test / jouleclaw-rs (pull_request) Has been cancelled
standards workspaces build + test / aac-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / joulecontract-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / map-rs (pull_request) Has been cancelled
standards workspaces build + test / mesh-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
spec citations resolve / Code cites no section its SPEC lacks (pull_request) Has been cancelled
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no comparison (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no company or product (pull_request) Has been cancelled
no-vanity-metrics / No new orphan source files (pull_request) Has been cancelled
lockfiles / every Cargo.lock is current (pull_request) Has been cancelled
SECURITY.md's remaining known gap: fuzzing was a seeded-random smoke test, with no coverage guidance for the manifest parser, the integer payload parsers or the codec wrappers. wai-rs/fuzz/ adds four libFuzzer targets (cargo-fuzz, AddressSanitizer): - envelope: version detection, the WAI1 and WAI2 readers, the listing form; - int_payload: int_payload::decode_int against each (capability, parameter set) pair the integer conformance corpus commits, the first byte choosing the pair; - media: the picture, audio, video and text decoders of the floor and the recommended set, the first byte choosing the decoder; - containers: the extension containers (world, causal op, film, reel, score, haptic, splat4d, audio scene). `seeds` builds the corpora from the repository: every committed envelope, integer payload and container, and a valid payload per decoder from the reference writers. `run.sh` runs each target for a given time, tells a find (exit 1, input kept in fuzz/artifacts) from a tool failure (exit 2), and builds with --no-cfg-fuzzing because rav1e depends on libfuzzer-sys under cfg(fuzzing), which left wai's cdylib with an undefined symbol. wai-fuzz.yml runs each target for 15 minutes nightly on the macOS runner (which has the codec libraries), never per pull request, keeping the corpus on the CI volume between nights. wai-rs's one multi-line inline table (c2pa) is put on one line: cargo-fuzz's TOML parser rejects it. Found in the containers target's first seconds: the audio scene reader reserved a u32 count of entries read from the input before reading one, so a few bytes asked for 48 GiB. The world, reel, score, haptic and splat4d readers did the same. Each now reserves no more than its remaining bytes could hold. tests/container_reservations.rs rewrites a committed vector of each to declare 2^32 - 1 entries in four bytes and requires a refusal with no single allocation past a few megabytes, through an allocator that records the largest request and refuses past 256 MiB; on the old readers the world asset table alone asked for 256 GiB (falsified). wai-conformance runs it on every leg. SECURITY.md now says what is fuzzed, and keeps two gaps: the C libraries are not instrumented, and the extension formats' declared durations have no reader limit yet (rendering allocates in proportion; reading is bounded). Smoke runs on the build host: envelope, int_payload and media 120 s each, nothing found; containers found the above, then ran 300 s clean once fixed. Green there: the new test, and the 275 lib tests with the container features. Clean: tests-in-CI, spec and section refs, workflow concurrency, lockfiles, vanity metrics, comparative prose, public names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
133 lines
6.1 KiB
Rust
133 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() {
|
|
let cases: [(&str, &str, u8, fn(&[u8]) -> bool); 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");
|
|
}
|
|
}
|