libfreemkv: fix rc.5.2 audit code findings
1. HEVC CRA->BLA false-trigger on 33-bit PTS wraparound (src/mux/codec/hevc.rs): the clip-boundary auto-detect compared the RAW 33-bit PES PTS against the high-water mark, so a single-clip title crossing 2^33->0 (~26.5h) false-armed pending_clip_boundary and rewrote a legitimate in-clip CRA(21)->BLA_W_LP(16), dropping valid RASL pictures (visible corruption) and breaking the single-clip byte-identical guarantee. Now unwrap the PTS onto a monotonic 64-bit timeline first (a near-full-period backstep is a wrap: add 2^33, update the watermark, do not arm). Regression test cra_after_33bit_pts_wrap_not_rewritten; the genuine-clip-join test still passes. 2. Single-pass recovery read bypassed the transport-failure abort (src/mux/disc.rs): the line-442 short-circuit only inspected the 10s read res. A transport failure (status 0xFF, wedged USB bridge) on the 60s recovery read fell into the skip_errors branch and zero-filled/ advanced, marching the disc at one bridge-recovery per probe (run-forever, hard rule #2). Re-check the recovery error for is_scsi_transport_failure() before the skip block and abort with Error::DiscRead. Test transport_failure_on_recovery_read_aborts_even_with_skip_errors. 3. Recovery-read SUCCESS branch had no coverage (src/mux/disc.rs tests): added RecoverableReader (errors when recovery=false, succeeds when recovery=true) and test recovery_read_success_muxes_recovered_data_no_skip driving fill_extents to the size-1 bottom-out and asserting the recovered data is muxed (counters advance, no skip). 4. TrueHD channel-correction probe omitted set_unit_base (src/disc/mod.rs correct_truehd_channels): the probe read via a DecryptingSectorSource without anchoring the AACS unit-alignment gate, so it degraded to absolute start_lba % 3 and returned DecryptFailed on a non-3-aligned extent, silently understating Atmos/7.1 as 5.1. Now call set_unit_base(ext.start_lba) before the probe read (no-op for CSS/None). 5. is_unit_aligned lba<unit_base latent trap (src/aacs/decrypt.rs): wrapping_sub mis-gated when lba < unit_base (2^32 == 1 mod 3). Switched to saturating_sub (clamps offset to 0, a unit boundary) and pinned the contract with is_unit_aligned_lba_below_base_is_well_defined plus is_unit_aligned_relative_to_base. cargo +1.86 fmt --check / clippy -D warnings / test --tests all green.
This commit is contained in:
+45
-1
@@ -29,8 +29,15 @@ pub const ALIGNED_UNIT_SECTORS: u32 = (ALIGNED_UNIT_LEN / SECTOR_LEN) as u32;
|
||||
/// disc whose clip `start_lba` is not itself 3-aligned would otherwise mis-gate
|
||||
/// (reject readable units, then report "Decryption failed") on exactly the
|
||||
/// titles whose clips land off a 3-boundary.
|
||||
///
|
||||
/// `lba` is always `>= unit_base` by contract (a read never begins before the
|
||||
/// extent base it is measured against). `saturating_sub` makes the `lba <
|
||||
/// unit_base` case well-defined anyway — it clamps the offset to 0, which is a
|
||||
/// unit boundary — rather than the latent `wrapping_sub` trap where an
|
||||
/// underflow wraps to ~2^32 and, because `2^32 ≡ 1 (mod 3)`, mis-reports the
|
||||
/// alignment (e.g. `lba == unit_base - 1` would falsely read as aligned).
|
||||
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
|
||||
lba.wrapping_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0
|
||||
lba.saturating_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0
|
||||
}
|
||||
|
||||
/// Size of one sector.
|
||||
@@ -331,6 +338,43 @@ mod tests {
|
||||
assert_eq!(dec, plain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_unit_aligned_relative_to_base() {
|
||||
// Aligned at the base and every 3 sectors above it; misaligned between.
|
||||
assert!(is_unit_aligned(100, 100), "base itself is aligned");
|
||||
assert!(is_unit_aligned(103, 100), "one unit past base is aligned");
|
||||
assert!(is_unit_aligned(106, 100));
|
||||
assert!(!is_unit_aligned(101, 100));
|
||||
assert!(!is_unit_aligned(102, 100));
|
||||
// Non-3-aligned base: alignment is RELATIVE to the base, not absolute.
|
||||
assert!(is_unit_aligned(101, 101), "non-3-aligned base is aligned");
|
||||
assert!(is_unit_aligned(104, 101));
|
||||
assert!(!is_unit_aligned(102, 101));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_unit_aligned_lba_below_base_is_well_defined() {
|
||||
// Latent-trap contract (rc.5.2 audit #5): a read never starts before its
|
||||
// extent base, but if `lba < unit_base` the result must be well-defined,
|
||||
// NOT the `wrapping_sub` underflow that — because 2^32 ≡ 1 (mod 3) —
|
||||
// would falsely report alignment. `saturating_sub` clamps to offset 0,
|
||||
// which is a unit boundary, so any `lba <= unit_base` reads as aligned.
|
||||
assert!(
|
||||
is_unit_aligned(99, 100),
|
||||
"lba just below base must not wrap"
|
||||
);
|
||||
assert!(is_unit_aligned(98, 100));
|
||||
assert!(is_unit_aligned(0, 100));
|
||||
// The specific wrapping_sub trap value: unit_base - 1. With wrapping_sub
|
||||
// this is 0xFFFF_FFFF % 3 == 0 → falsely "aligned" by underflow; with
|
||||
// saturating_sub it is genuinely 0 → aligned, for the right reason.
|
||||
assert!(is_unit_aligned(u32::MAX, u32::MAX)); // base == lba, trivially aligned
|
||||
assert!(
|
||||
is_unit_aligned(0, u32::MAX),
|
||||
"max base, lba 0 must saturate to 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_unit_unencrypted() {
|
||||
// A clear unit (TS syncs intact) is not scrambled → passes through.
|
||||
|
||||
Reference in New Issue
Block a user