libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening

Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
This commit is contained in:
Matthew Jackson
2026-09-02 12:40:55 -07:00
parent 962aa5d852
commit 6e14fc0c0c
319 changed files with 25069 additions and 18952 deletions
+9 -23
View File
@@ -77,11 +77,8 @@ fn css_is_scrambled_detection() {
// ── AACS Public API Tests ───────────────────────────────────────────────────
/// Test 6: aacs_decrypt_unit_roundtrip
///
/// Build a synthetic 6144-byte aligned unit with TS sync bytes, encrypt it
/// using the AACS algorithm (AES-ECB header derivation + AES-CBC body),
/// then decrypt with decrypt_unit() and verify the plaintext matches.
// Test 6: build a synthetic aligned unit with TS sync bytes, encrypt via
// AACS, decrypt with decrypt_unit(), and verify the plaintext matches.
#[test]
fn aacs_decrypt_unit_roundtrip() {
use aes::Aes128;
@@ -209,10 +206,8 @@ fn aacs_disc_hash_deterministic() {
);
}
/// Test: aacs_decrypt_unit_key_roundtrip
///
/// Verify that encrypting a unit key with AES-ECB and decrypting it with
/// decrypt_unit_key recovers the original.
// Test: verify that encrypting a unit key with AES-ECB and decrypting it
// with decrypt_unit_key recovers the original.
#[test]
fn aacs_decrypt_unit_key_roundtrip() {
use aes::Aes128;
@@ -308,12 +303,9 @@ fn aacs_is_clean_detection() {
);
}
/// Test: aacs_clear_unit_reports_not_encrypted
///
/// `decrypt_unit` is now PURE (applies the key unconditionally). The "leave a
/// clear unit untouched" policy lives at the caller's gate `aacs_unit_encrypted`:
/// a CPI-clear unit reports not-encrypted, so the caller never hands it to
/// decrypt_unit.
// Test: decrypt_unit is PURE (applies the key unconditionally). The "leave
// a clear unit untouched" policy lives at the caller's gate
// aacs_unit_encrypted: a CPI-clear unit reports not-encrypted.
#[test]
fn aacs_clear_unit_reports_not_encrypted() {
let mut unit = vec![0x42u8; aacs::content::ALIGNED_UNIT_LEN];
@@ -559,14 +551,8 @@ fn css_roundtrip_multiple_keys() {
// ── CSS keyless recovery tests ─────────────────────────────────────────────
/// Attempt the keyless recovery on synthetically scrambled sectors.
///
/// The CSS cipher on real DVDs stores ciphertext through a TAB1 output
/// layer that the keyless recovery depends on. Synthetically scrambled
/// sectors (produced by calling descramble_sector on plaintext) may not
/// exhibit this relationship, so the attack is not guaranteed to converge
/// on synthetic data. This test verifies that when the attack DOES return
/// a key, that key correctly descrambles the sector.
// Attempt keyless recovery on synthetically scrambled sectors; see
// docs/css-keyless.md for why synthetic data may not converge.
#[test]
fn css_keyless_recovery_validates_cracked_key() {
let candidates: &[([u8; 5], [u8; 5])] = &[
+2 -14
View File
@@ -587,20 +587,8 @@ fn aacs_dir_alone_marks_the_disc_encrypted_and_reports_the_capture_error() {
);
}
/// The scan reports the medium's size on BOTH axes it exposes, derived from the
/// one sector count the caller hands in:
///
/// * `capacity_bytes` is that sector count times the 2048-byte logical sector
/// (ECMA-167 / BD-ROM logical block size). It is what sizes a full-disc image
/// read and what the progress percentage divides by, so a wrong scale is a
/// wrong ISO length, not a cosmetic number.
/// * `layers` distinguishes single- from dual-layer media. The threshold sits
/// between the two real capacities: a single-layer BD-25 is 12,219,392
/// sectors (25,025,314,816 bytes / 2048) and a dual-layer BD-50 is 24,438,784
/// sectors, so BD-25 must report 1 layer and BD-50 must report 2.
///
/// `scan_image` takes the sector count as a parameter, so this exercises the
/// real derivation without a 50 GB fixture.
// Capacity/layer derivation from sector count; wrong scale = wrong ISO length.
// See docs/disc-tests.md — capacity_bytes and layers threshold rationale.
#[test]
fn scan_image_reports_capacity_in_bytes_and_the_layer_count() {
let opts = ScanOptions::default();
+3 -6
View File
@@ -313,12 +313,9 @@ fn fvi_sink_indexes_real_mpeg2_pipeline_output() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Codec-agnostic path: a non-MPEG2 stream (HEVC/H.264/VC-1) emits frames with
/// `coding == None` but real `keyframe` + `source` + `pts`. The `.fvi` records
/// must still be USEFUL — `key`/`type` from the frame's keyframe flag, `src`/
/// `pts` populated — NOT degraded to `type:"?"`/`src:null` just because
/// `PictureInfo` is MPEG-2-specific. The genuine null/"P"-fallback path only
/// fires when a field is truly absent (no provenance / non-key). No panic.
// Codec-agnostic path: non-MPEG2 frames (`coding == None`) with real keyframe/
// source/pts must still yield useful `.fvi` records, not degrade to
// `type:"?"`/`src:null` just because `PictureInfo` is MPEG-2-specific.
#[test]
fn fvi_sink_indexes_non_mpeg2_frames_codec_agnostically() {
use libfreemkv::pes::{PesFrame, SourcePos};
+10 -38
View File
@@ -1,37 +1,13 @@
//! Integration tests for the SCSI error-decoding contract.
//!
//! v0.13.20 rewrote `scsi/linux.rs` to a synchronous blocking SG_IO and
//! consolidated sense parsing into the `parse_sense` helper that every
//! platform backend now shares. v0.13.23 replaced the `Error::ScsiError`
//! flat-fields shape with `{ opcode, status, sense: Option<ScsiSense> }`
//! so callers can route on structured sense data (key + ASC + ASCQ) via
//! [`Error::scsi_sense`] / [`Error::is_marginal_read`] /
//! [`ScsiSense::is_*`].
//! Pins the contract every backend must satisfy via a mock `ScsiTransport`:
//! healthy result → `Ok(ScsiResult { .. })`; transport-level failure →
//! `Error::ScsiError { status: SCSI_STATUS_TRANSPORT_FAILURE, sense: None }`
//! ([`Error::is_scsi_transport_failure`] true); SCSI-level failure (CHECK
//! CONDITION) → `Error::ScsiError` with parsed key/ASC/ASCQ, checked via
//! [`Error::is_marginal_read`] / [`ScsiSense::is_*`].
//!
//! The actual `ioctl(SG_IO, ...)` call is impossible to mock without a
//! kernel, so libc shims are deliberately avoided here. These tests
//! therefore pin the *contract* every backend must satisfy via a mock
//! `ScsiTransport`:
//!
//! 1. Healthy result → `Ok(ScsiResult { bytes_transferred = data.len() - resid })`.
//! 2. Transport-level failure (no SCSI status delivered: kernel
//! timeout, USB bridge wedge, IOKit service error) →
//! `Error::ScsiError { status: SCSI_STATUS_TRANSPORT_FAILURE, sense: None }`.
//! `Error::is_scsi_transport_failure()` returns `true`. Used by
//! `drive_has_disc` to detect the wedge signature.
//! 3. SCSI-level failure (drive replied CHECK CONDITION with sense) →
//! `Error::ScsiError { status: 0x02, sense: Some(ScsiSense {…}) }`
//! with the parsed key/ASC/ASCQ.
//! 4. `Error::is_marginal_read()` is `true` for MEDIUM ERROR /
//! ABORTED COMMAND / RECOVERED ERROR / NO SENSE; `false` for
//! HARDWARE / DATA PROTECT / UNIT ATTENTION / NOT READY / ILLEGAL
//! REQUEST and for transport failures.
//!
//! Inline `parse_sense_tests` in `src/scsi/mod.rs` cover the pure parse
//! logic (descriptor 0x72/0x73 vs fixed 0x70/0x71, short-buffer, VALID
//! bit masking, unknown response codes, ASC/ASCQ offsets); this file
//! covers the consumer side — a real transport feeding a real Error
//! variant to a real call site (`scsi::inquiry`).
//! See docs/scsi-error-decoding.md for rationale and history.
use libfreemkv::error::Error;
use libfreemkv::scsi::{
@@ -41,13 +17,9 @@ use libfreemkv::scsi::{
SENSE_KEY_RECOVERED_ERROR, SENSE_KEY_UNIT_ATTENTION, ScsiResult, ScsiSense, ScsiTransport,
};
/// A scripted ScsiTransport. Each `execute()` consumes the next entry
/// from `script` and returns the corresponding outcome.
///
/// Outcomes mirror what each backend's `execute()` should produce after
/// the v0.13.23 sense plumbing: `Option<ScsiSense>` carrying the full
/// SPC-4 triple for drive-reported failures, `None` for transport-level
/// failures.
// A scripted ScsiTransport. Each `execute()` consumes the next entry
// from `script` and returns the corresponding outcome.
// See docs/scsi-error-decoding.md — MockTransport outcomes.
struct MockTransport {
script: Vec<MockOutcome>,
next: usize,