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:
@@ -142,6 +142,17 @@ impl PrefetchedSectorSource {
|
||||
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
|
||||
});
|
||||
}
|
||||
// A zero alignment is the sibling programming error, and it is worse: the
|
||||
// producer thread reaches `remaining % unit_align` and panics with a
|
||||
// divide-by-zero, which `catch_unwind` then reports as
|
||||
// `DemuxThreadPanicked` — a panic printed through the process hook and a
|
||||
// misleading error, out of a public constructor that returned `Ok`. Reject
|
||||
// it here, exactly as `batch_sectors == 0` is rejected.
|
||||
if unit_align == 0 {
|
||||
return Err(crate::error::Error::IoError {
|
||||
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
|
||||
});
|
||||
}
|
||||
// Accumulate in u64 then clamp: extents can derive from
|
||||
// untrusted nav/MPLS/UDF data, so a naive u32 `sum()` could
|
||||
// panic in debug / wrap in release on a hostile total. The
|
||||
@@ -667,6 +678,29 @@ mod tests {
|
||||
assert!(err.is_err(), "zero batch_sectors must be rejected");
|
||||
}
|
||||
|
||||
/// `unit_align == 0` must be rejected by the constructor, not turned into a
|
||||
/// divide-by-zero panic on the producer thread. Before the guard,
|
||||
/// `new_with_events` returned `Ok` and the producer evaluated
|
||||
/// `remaining % 0`, panicking ("attempt to calculate the remainder with a
|
||||
/// divisor of zero"); `catch_unwind` then reported the read as
|
||||
/// `DemuxThreadPanicked` instead of the `InvalidInput` its sibling parameter
|
||||
/// gets — a panic printed out of a public constructor's own thread.
|
||||
#[test]
|
||||
fn zero_unit_align_rejected() {
|
||||
let res = PrefetchedSectorSource::new_with_events(
|
||||
EndlessZeroSource,
|
||||
big_extent(),
|
||||
4096,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let Err(crate::error::Error::IoError { source }) = res else {
|
||||
panic!("zero unit_align must be rejected with InvalidInput");
|
||||
};
|
||||
assert_eq!(source.kind(), std::io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
/// More than 3 sequential direct `read_sectors` calls must succeed. The
|
||||
/// recycle pool seeds PREFETCH_CHANNEL_DEPTH+1 (3) buffers; before
|
||||
/// the fix the direct path dropped each drained buffer, so the 4th
|
||||
|
||||
Reference in New Issue
Block a user