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:
+57
-4
@@ -130,10 +130,22 @@ impl TimelineContinuity {
|
||||
// within a backstep below the frontier keeps the remap to genuine
|
||||
// tail stragglers; a long audio-only tail, a sparse subtitle, or an
|
||||
// EL frame that simply runs ahead is left on the current offset.
|
||||
//
|
||||
// Every comparison below saturates. `high` is derived from an
|
||||
// untrusted container timestamp (an `mkv://` source's
|
||||
// CLUSTER_TIMESTAMP × TimestampScale is clamped only against
|
||||
// `i64::MAX`, so a hostile file can put the frontier AT `i64::MAX`),
|
||||
// and `raw_pts_ns` can be negative (a SimpleBlock's signed relative
|
||||
// timestamp). Plain `high + BACKSTEP` / `high - BACKSTEP` would then
|
||||
// overflow: a panic out of the public `Stream::write` path in an
|
||||
// overflow-checked build, and in release a wrap to the opposite sign
|
||||
// that fires the straggler clamp on essentially every passive frame.
|
||||
if let Some(high) = self.high_ns {
|
||||
if mapped > high + DISCONTINUITY_BACKSTEP_NS {
|
||||
if mapped > high.saturating_add(DISCONTINUITY_BACKSTEP_NS) {
|
||||
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
|
||||
if prev_mapped <= high && prev_mapped >= high - DISCONTINUITY_BACKSTEP_NS {
|
||||
if prev_mapped <= high
|
||||
&& prev_mapped >= high.saturating_sub(DISCONTINUITY_BACKSTEP_NS)
|
||||
{
|
||||
return prev_mapped;
|
||||
}
|
||||
}
|
||||
@@ -147,12 +159,17 @@ impl TimelineContinuity {
|
||||
return adj;
|
||||
};
|
||||
let adj = raw_pts_ns.saturating_add(self.offset_ns);
|
||||
if adj < high - DISCONTINUITY_BACKSTEP_NS {
|
||||
if adj < high.saturating_sub(DISCONTINUITY_BACKSTEP_NS) {
|
||||
// Clip-boundary reset (real multi-clip seam): continue just after the
|
||||
// frontier. Save the previous offset so a lagging non-video tail
|
||||
// frame can be recognised and remapped to the seam (see above).
|
||||
self.prev_offset_ns = self.offset_ns;
|
||||
let bump = (high - adj).saturating_add(DISCONTINUITY_GAP_NS);
|
||||
// `high - adj` is a backward step, so positive — but both ends are
|
||||
// untrusted (`high` up to i64::MAX, `adj` down to i64::MIN), so
|
||||
// saturate rather than panic in a checked build.
|
||||
let bump = high
|
||||
.saturating_sub(adj)
|
||||
.saturating_add(DISCONTINUITY_GAP_NS);
|
||||
self.offset_ns = self.offset_ns.saturating_add(bump);
|
||||
let adj2 = raw_pts_ns.saturating_add(self.offset_ns);
|
||||
self.high_ns = Some(high.max(adj2));
|
||||
@@ -464,4 +481,40 @@ mod tests {
|
||||
"frame must stay in the new epoch (> frontier), got {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A saturated frontier must not panic the muxer. An `mkv://` source's
|
||||
/// tick→ns multiply saturates at `i64::MAX` (mkvstream's `parse_block`), so a
|
||||
/// hostile TimestampScale/CLUSTER_TIMESTAMP puts `high_ns` AT `i64::MAX`.
|
||||
/// Every subsequent PASSIVE frame then evaluated `high + BACKSTEP`, which
|
||||
/// panicked ("attempt to add with overflow") out of the public
|
||||
/// `Stream::write` path in any overflow-checked build.
|
||||
#[test]
|
||||
fn saturated_frontier_does_not_overflow_on_passive_frame() {
|
||||
let mut tc = TimelineContinuity::new();
|
||||
// Video establishes the frontier at the saturation point.
|
||||
assert_eq!(adj_video(&mut tc, i64::MAX), i64::MAX);
|
||||
assert_eq!(tc.high_ns, Some(i64::MAX));
|
||||
// Passive frame: `high + BACKSTEP` overflowed here.
|
||||
let out = adj_other(&mut tc, 0);
|
||||
assert_eq!(out, 0, "a passive frame keeps its own mapping");
|
||||
// And a passive frame AT the frontier: `high - BACKSTEP` is the other
|
||||
// unchecked side of the straggler discriminator.
|
||||
assert_eq!(adj_other(&mut tc, i64::MAX), i64::MAX);
|
||||
}
|
||||
|
||||
/// The epoch-decision side of the same arithmetic: `adj < high - BACKSTEP`
|
||||
/// and the `high - adj` bump both took untrusted ends. A frontier at
|
||||
/// `i64::MIN`-adjacent values (a negative SimpleBlock-relative timestamp) and
|
||||
/// a `i64::MAX` frontier are both reachable from container data.
|
||||
#[test]
|
||||
fn extreme_video_pts_does_not_overflow_the_epoch_bump() {
|
||||
let mut tc = TimelineContinuity::new();
|
||||
assert_eq!(adj_video(&mut tc, i64::MAX), i64::MAX);
|
||||
// Hard backward jump to the negative extreme: `high - adj` overflowed.
|
||||
let out = adj_video(&mut tc, i64::MIN);
|
||||
// Saturated bump (`i64::MAX`) applied to `i64::MIN` → -1, and the
|
||||
// frontier never regresses.
|
||||
assert_eq!(out, -1);
|
||||
assert_eq!(tc.high_ns, Some(i64::MAX));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user