mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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:
+9
-23
@@ -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
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user