Fix fifteen defects across perf, resource, panics and key hygiene
All 21 findings held up under verification; 15 fixed here, 6 deferred to files another agent held this round, 0 rejected. **A defect in my own round-2 probe fix.** CHUNK_SECTORS was 1024, and 1024 % 3 == 1 — verified — so every chunk after the first was misaligned against the 6144-byte AACS aligned unit and would be REJECTED by DecryptingSectorSource's alignment gate. On an encrypted disc the forced-subtitle probe I added last round would have read almost nothing past its first chunk. Now 1023 sectors (341 aligned units) with a const assertion that fails the build if it stops dividing, plus set_unit_base per extent so the source's gate is anchored where the extent actually starts. **The same probe skipped sectors on a short read**, advancing by the REQUESTED count rather than the bytes actually returned, so a partial read silently left a gap in the middle of the evidence. It now advances by n/SECTOR_BYTES and clamps n to the buffer. **Its cache key omitted the PGS PID set**, so a playlist declaring an extra subtitle PID got another playlist's verdict for a track that had never been probed. And the key was the whole extent list, so partial clip sharing missed entirely. Both fixed by keying (start_lba, sector_count, pid) — and per-extent keying was shown SOUND rather than assumed: ForcedTracker is two monotone booleans, so per-extent evidence composes by field-wise OR, order- and grouping-independently. Making that honest required per-extent demux state, so an extent's evidence comes only from its own bytes, and memoising only extents whose read reached a designed stop. **A reachable panic in the timeline.** mkvstream::parse_block accepts a TimestampScale up to i64::MAX, so a video frame can set high_ns = i64::MAX and the next passive frame panicked adding the backstep. In release it wrapped negative instead, firing the straggler clamp for essentially every passive frame — audio and subtitles rewritten onto the wrong point of the output timeline. All four sites saturate. **A public constructor divided by zero**: PrefetchedSectorSource::new_with_events with unit_align == 0. Now InvalidInput, matching its batch_sectors sibling. **Two Debug impls printed key material.** DiscInputs (volume_id, mkb, unit_key_ro, samples) and UnitKeyFile both derived Debug. Nothing logs them today — fixed as prevention, because the next tracing::debug! someone adds is the leak. A doc claim that DiscInputs "contains no secrets" was false and is corrected. **An env-var multiply could overflow** in file_sector_source; now bounded at 64 GiB like its writeback sibling, with the parse split out so the bound is testable without touching process env. **The mp4 demuxer allowed one sample per file byte** — ~64x RAM amplification. Now file_len/16, since only vide/soun tracks are indexed and the shortest legal AC-3 frame is 128 bytes. **Two pipeline concurrency defects**: a consumer apply() error was invisible to the producer, and abandon/finalise had a TOCTOU where a caller could report an unfinalised output. Both fixed with compare-exchange state rather than a bool. **Two per-frame copies removed**, both MEASURED rather than reasoned: the AU assembler now hands its allocation to the frame (same pointer, unchanged capacity, proven by asserting the pointer) and tsmux reuses one Annex-B buffer across frames. Both keep capacity deliberately — a naive split_off would have cost more than it saved. **A comment pointed at the wrong file** for a mirrored constant; the mirror is now compiler-enforced with a const assertion converting 90 kHz ticks to ns, so drift fails the build. Deferred to another agent's files, all confirmed: detect_rate's fractional-twin snap, the mp4 reserve's u32 truncation, round_up_grain's overflow, the quadratic base-key gap fill, and MkvStream's frame cap counting frames rather than bytes. Every fix verified red by reverting it. Also noted for later: DecodeSampleSet still derives Debug over multi-MB of on-disc ciphertext.
This commit is contained in:
+83
-2
@@ -321,8 +321,7 @@ impl AuAssembler {
|
||||
self.disc_marks.pop_front();
|
||||
}
|
||||
|
||||
let data = self.buf[..end].to_vec();
|
||||
self.buf.drain(..end);
|
||||
let data = self.take_front(end);
|
||||
self.base += end as u64;
|
||||
self.reset_scan();
|
||||
out.push(AssembledAu {
|
||||
@@ -336,6 +335,41 @@ impl AuAssembler {
|
||||
out
|
||||
}
|
||||
|
||||
/// Detach `buf[..end]` as the emitted AU's own `Vec` and leave `buf` holding
|
||||
/// the tail.
|
||||
///
|
||||
/// The AU's bytes are HANDED OVER — `buf`'s allocation becomes the returned
|
||||
/// `Vec` and a fresh buffer (pre-sized to the same capacity, so the next AU
|
||||
/// accumulates without re-growing) takes its place holding only the short
|
||||
/// tail. `buf[..end].to_vec()` + `drain(..end)` instead copied every AU out
|
||||
/// in full: on a UHD HEVC title that is a whole-frame memcpy (hundreds of KB)
|
||||
/// per coded picture, ~200k times, for bytes that are about to be discarded
|
||||
/// from `buf` anyway.
|
||||
///
|
||||
/// The allocation COUNT is unchanged (one per AU either way — the frame `Vec`
|
||||
/// before, the replacement buffer now), so the only difference is the copy
|
||||
/// that no longer happens. Nothing depends on `buf` keeping its identity: the
|
||||
/// only state tied to `buf[0]`'s position is `base`/`scan_pos`/`opener_pos`,
|
||||
/// which the caller updates immediately after.
|
||||
///
|
||||
/// Falls back to a copy when the buffer's capacity is far larger than the AU
|
||||
/// (a small AU after a multi-MB one): handing over would otherwise attach an
|
||||
/// oversized idle allocation to a small frame for as long as the frame queues
|
||||
/// downstream, trading a copy for resident memory.
|
||||
fn take_front(&mut self, end: usize) -> Vec<u8> {
|
||||
let cap = self.buf.capacity();
|
||||
if cap > end.saturating_mul(2) {
|
||||
let data = self.buf[..end].to_vec();
|
||||
self.buf.drain(..end);
|
||||
return data;
|
||||
}
|
||||
let mut tail = Vec::with_capacity(cap.max(self.buf.len() - end));
|
||||
tail.extend_from_slice(&self.buf[end..]);
|
||||
let mut data = std::mem::replace(&mut self.buf, tail);
|
||||
data.truncate(end);
|
||||
data
|
||||
}
|
||||
|
||||
/// Reset the incremental boundary-scan cursor. Called whenever `buf[0]` moves
|
||||
/// (an AU drained, or leading bytes discarded) so the next scan starts fresh
|
||||
/// from the new AU opener.
|
||||
@@ -841,4 +875,51 @@ mod tests {
|
||||
"over-cap AU is force-flushed, not buffered forever"
|
||||
);
|
||||
}
|
||||
|
||||
/// MEASURED: a drained AU must be HANDED the accumulation buffer's
|
||||
/// allocation, not copied out of it. The emitted `Vec`'s data pointer is the
|
||||
/// buffer's own pointer — which is only true if no full-frame copy happened.
|
||||
/// (`buf[..end].to_vec()` allocates fresh, so the pointers differ.) One
|
||||
/// whole-AU memcpy per coded picture is ~200k memcpys of a few hundred KB
|
||||
/// each on a UHD feature.
|
||||
#[test]
|
||||
fn drained_au_takes_over_the_buffer_allocation_without_copying() {
|
||||
let mut a = AuAssembler::for_codec(Codec::H264);
|
||||
// An AU large enough that the buffer's capacity is not >2x its size (the
|
||||
// small-AU copy path exists so a small frame cannot carry an oversized
|
||||
// idle allocation downstream).
|
||||
let au1 = au(0x11, 400 * 1024);
|
||||
let au2 = au(0x22, 400 * 1024);
|
||||
let mut stream = au1.clone();
|
||||
stream.extend_from_slice(&au2);
|
||||
|
||||
// Push everything except the final byte of AU2's delimiter, so no AU has
|
||||
// been emitted yet but the buffer holds the whole of AU1.
|
||||
a.push(&stream[..au1.len() + 3], Some(1), None, None, false);
|
||||
let before = a.buf.as_ptr();
|
||||
let cap_before = a.buf.capacity();
|
||||
let out = a.push(
|
||||
&stream[au1.len() + 3..au1.len() + 4],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(
|
||||
out[0].data, au1,
|
||||
"handover must preserve the AU bytes exactly"
|
||||
);
|
||||
assert_eq!(
|
||||
out[0].data.as_ptr(),
|
||||
before,
|
||||
"the emitted AU must own the buffer's allocation (no whole-frame copy)"
|
||||
);
|
||||
assert_eq!(
|
||||
a.buf.capacity(),
|
||||
cap_before,
|
||||
"the replacement buffer keeps the capacity, so the next AU does not re-grow"
|
||||
);
|
||||
assert_eq!(a.buf.len(), 4, "the buffer holds only AU2's delimiter tail");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user