Round 4: fix 26 defects across crypto, resource use and codec paths

Twenty-six confirmed findings from the fourth audit round, landed as one
cluster because they were found by agents working over disjoint file sets.

The one worth calling out is a pair of AACS tests that could not fail.
Both asserted CBC behaviour against a hand-rolled expectation that
happened to be IV-independent, so replacing AACS_IV with sixteen zero
bytes left them passing — they were pinning the code's own arithmetic,
not the published constant. Replaced with a literal witness of the
published IV plus the NIST SP 800-38A F.2.2 CBC-AES128 vector, and
verified the other way round: zeroing AACS_IV now fails three tests.

The rest are allocation and correctness work on hot paths: the Annex-B
writer in demux_sink allocated and freed a whole-frame Vec per frame,
which for a UHD title is ~200,000 allocations over the mmap threshold
plus the page faults to first-touch each one; it now reuses a buffer on
the writer, and still takes the NAL prefix width from the configuration
record rather than assuming four.

Six findings whose real fix lives in a consumer crate are recorded for
re-filing rather than patched here.
This commit is contained in:
Matthew Jackson
2026-07-29 22:09:52 -07:00
parent 4fcd28b487
commit 0bbceed985
19 changed files with 1342 additions and 126 deletions
+70 -12
View File
@@ -403,10 +403,32 @@ impl Drop for PrefetchShell {
impl Drop for PrefetchedSectorSource {
fn drop(&mut self) {
// Dropping the receiver closes the channel, which makes the
// next producer `send` return Err and exits the loop. Joining
// here gives us a deterministic shutdown — no detached thread
// can outlive the source.
// Drop the channel endpoints BEFORE joining the producer.
//
// `rx` and `recycle_tx` are sibling fields, so they are dropped only
// AFTER this `Drop::drop` body returns. Joining first therefore joined
// while both endpoints were still alive: a producer parked in the plain
// blocking `tx.send(Ok(buf))` (no timeout, so the `Halt` is never
// re-polled) never observed a disconnect and never returned, and the
// dropping thread blocked in `join()` forever. Any source dropped before
// its extents were drained — an error path, an operator stop, or a
// consuming crate using the public `new` + direct `read_sectors` — hit a
// permanent two-thread deadlock. `BytePrefetcher::drop` already had this
// shape; see `drop_undrained_source_joins_cleanly`.
//
// The endpoints are moved out via `mem::replace` with already-disconnected
// stand-ins (each stand-in's peer is dropped immediately), which drops the
// real ones here and needs neither `Option` fields nor `unsafe` — and
// leaves `into_channels`'s `ptr::read` moves untouched.
let (dead_tx, dead_rx) = bounded::<Batch>(0);
drop(dead_tx);
drop(std::mem::replace(&mut self.rx, dead_rx));
let (dead_send, dead_recv) = bounded::<Vec<u8>>(0);
drop(dead_recv);
drop(std::mem::replace(&mut self.recycle_tx, dead_send));
// Now the producer's next `send`/`recv` returns Err and its loop exits;
// joining gives a deterministic shutdown — no detached thread can outlive
// the source.
if let Some(h) = self.producer.take() {
let _ = h.join();
}
@@ -615,6 +637,42 @@ mod tests {
}
}
/// Regression: dropping a `PrefetchedSectorSource` DIRECTLY — the
/// public `new` + documented direct-read path, and any error/halt
/// exit before the extents are drained — must join the producer
/// cleanly. `Drop::drop` used to `join()` while the struct still
/// held `rx` and `recycle_tx` (sibling fields drop only AFTER
/// `Drop::drop` returns), so a producer parked in the plain blocking
/// `tx.send(Ok(buf))` never saw a disconnect and the dropping thread
/// blocked in `join()` forever — a permanent two-thread deadlock that
/// cancelling the `Halt` could not escape, because that `send` has no
/// timeout and never re-polls the token.
///
/// 300 sectors at batch=3 is 100 batches against a forward channel of
/// depth `PREFETCH_CHANNEL_DEPTH` (2), so the producer is guaranteed
/// to be blocked in `send` by the time the drop runs. Mirrors
/// `byte_prefetcher::drop_endless_prefetcher_joins_cleanly`, whose
/// `Drop` already had the correct shape.
#[test]
fn drop_undrained_source_joins_cleanly() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 300,
}];
let halt = Halt::new();
let pf = PrefetchedSectorSource::new(
PatternSource { capacity: 9999 },
extents,
3,
Some(halt.clone()),
)
.expect("spawn");
// Drop without draining a single batch — the old Drop deadlocked here.
drop(pf);
});
}
/// The CRITICAL regression: after `into_channels`, dropping the
/// returned forward receiver + recycle sender must let the producer
/// observe disconnection and exit, so dropping the `PrefetchShell`
@@ -1002,11 +1060,11 @@ mod tests {
let pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
// 9 + 6 + 3 = 18, independent of inner source capacity.
assert_eq!(pf.capacity_sectors(), 18);
// Release the producer without draining: peel the channels
// and drop them so the producer observes disconnection
// (dropping `pf` directly would join while still holding the
// channels → deadlock; the production drain path always uses
// into_channels).
// Release the producer without draining via the production
// zero-copy path: peel the channels and drop them so the
// producer observes disconnection. (A direct `drop(pf)` is
// also safe now — see `drop_undrained_source_joins_cleanly`
// — but the mux always uses into_channels.)
let (rx, recycle_tx, shell) = pf.into_channels();
drop(rx);
drop(recycle_tx);
@@ -1042,9 +1100,9 @@ mod tests {
u32::MAX,
"summed total must saturate at u32::MAX, not wrap"
);
// Release the producer via into_channels + drop (a direct
// drop of `pf` would join while still holding the channels →
// deadlock against the still-running EndlessZeroSource).
// Release the producer via into_channels + drop, the production
// zero-copy path. (A direct `drop(pf)` also joins cleanly now —
// see `drop_undrained_source_joins_cleanly`.)
let (rx, recycle_tx, shell) = pf.into_channels();
drop(rx);
drop(recycle_tx);