Matthew Jackson 8b8bcff106 test: pin five untrusted-input guards in the AACS 2.1 and CSS paths
Second pass over src/aacs and src/css. No production change; the only
non-test edits are two fixture bytes and one test rename.

Five latent panics on untrusted data, every guard correct and none
tested — so each was free to be deleted:

  variant.rs:224  a 0x04 record not a multiple of 5 indexes p_uv[0..4]
                  off a one-byte tail
  variant.rs:269  a 0x0c record shorter than the 0x04 slot count
                  slices past the cvalue table
  stevenson.rs:177  short sector read -> index 138 into a 129-byte slice
  stevenson.rs:208  a crib longer than the 1920-byte encrypted region
                    -> index 2058 into 2048
  stevenson.rs:272  a header periodic all the way to offset 0 ->
                    subtract with overflow

That last one is reachable from ORDINARY DVD data — constant or padding
bytes are periodic. Verified on HEAD: widening the guard to <= 0x80
passes all 64 css tests unmutated.

media_key_variant_from_kp had only a soft-correction test, so every
step past that early return was unexecuted. The new two-slot fixture
puts the covering slot at index 1, so the uvs[1 + 5*idx] and
cvalues[idx*16] strides stop multiplying by zero.

derive.rs:319 + -> - confirmed killable, as the first pass predicted:
p == 0 makes (p-1)..32 underflow. Every prior fixture used a uv whose
lowest set bit was 4, 10 or 11, so trailing_zeros() was never 0.

One fixture bug caught and fixed rather than papered over: a |= mutant
first SURVIVED because mk[14]'s 0x04 bit happened to be set, making OR
and XOR agree. The byte is now clear and an assert_eq! pins it, so the
fixture cannot drift back into agreeing with the mutation it exists to
catch.

walk_mkb_be24_high_byte_is_honored renamed to
walk_mkb_be24_middle_byte_is_honored. Its 0x00_0110 length exercises
the << 8 term only, which is why << 16 -> >> 16 survived it. The name
was the lie; both framings are worth having, and the comment now points
at the genuine high-byte test at 0x01_0004.

derive.rs 146:32 and 154:30 stay untested, now with a proof rather than
a judgement: bit_pos == -1 requires current_v_mask == 0xFFFF_FFFF, and
calc_v_mask can never return that — its loop condition holds at
!v_mask == 0, so it always shifts at least once. Both branches are
reachable only after the walk has gone non-convergent and is heading
for the bounded exit, where the return value is undefined. Termination
is already pinned.

Equivalents proven by observing green, including six more OR/XOR pairs
on provably disjoint bit fields, and the two KEY_CORRECTION_DATA sites
where the constant is the documented all-zero placeholder so x ^ 0 ==
x | 0. Those become killable only if a real per-licensee KCD is wired
in.

A partial confirmation sweep (138 of 415 mutants before the box
saturated) found 135 caught, one timeout that is itself a detection,
and exactly one survivor — the KEY_CORRECTION_DATA equivalent above.
2026-07-30 16:27:03 -07:00
2026-04-16 17:51:45 +00:00

License: MIT

libfreemkv

Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Drive-level unlocking is handled internally; consumers work with disc access and decryption only.

DVDs (CSS) decrypt out of the box. Blu-ray and UHD (AACS) require a keydb.cfg (default ~/.config/freemkv/keydb.cfg) supplying disc-specific volume unique keys; no AACS key material is compiled in.

12+ MB/s sustained read speeds on BD. Drive prep (init()) handles unlocking internally via the freemkv-unlock crate — clients never see it; when no drive unlock applies, the library rips via the host-certificate AACS handshake.

Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.

Source & API · Technical Docs

Part of the freemkv project.

Install

Consumed by git tag (not published to crates.io):

[dependencies]
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "vX.Y.Z" }

Quick Start

use libfreemkv::{Drive, Disc, ScanOptions};
use std::path::Path;

// Open drive — identified via INQUIRY
let mut drive = Drive::open(Path::new("/dev/sg4"))?;
drive.wait_ready()?;              // wait for disc
drive.init()?;                     // unlock + prep (handled internally)
drive.probe_disc()?;               // probe disc surface for optimal speeds

// Scan disc — UDF, playlists, streams, AACS (all automatic)
let disc = Disc::scan(&mut drive, &ScanOptions::default())?;

for title in &disc.titles {
    println!("{}{} streams", title.duration_display(), title.streams.len());
}

// Stream pipeline — read PES frames from any source, write to any output
let opts = libfreemkv::InputOptions::default();
let mut input = libfreemkv::input("iso://Disc.iso", &opts)?;
let title = input.info().clone();
let mut output = libfreemkv::output("mkv://Movie.mkv", &title)?;
while let Ok(Some(frame)) = input.read() {
    output.write(&frame)?;
}
output.finish()?;

Multi-pass recovery rip

Recovery moved OUT of this crate in 1.6.0. The sweep/patch strategy, the ddrescue mapfile, damage classification and the multipass loop now live in the freemkv-engine crate as freemkv_engine::recovery::{copy, sweep, patch}.

libfreemkv keeps the layers underneath: the raw single-shot read (Drive::read) and the SCSI-fact translation (SenseFamily) that the engine's strategy is built on. The dependency runs engine → libfreemkv, so this crate cannot call into it; front-ends get recovery from the engine directly. See docs/rip-recovery.md for what stayed here.

What It Does

  • Drive access — open, identify, internal unlock + prep, speed control, eject
  • 12+ MB/s reads — auto-detects kernel transfer limits, sustained full speed
  • Disc scanning — UDF 2.50 filesystem, MPLS playlists, CLPI clip info
  • Stream labels — 5 BD-J format parsers (Paramount, Criterion, Pixelogic, CTRM, Deluxe)
  • AACS decryption — transparent key resolution and content decrypt (1.0 + 2.0 bus decryption)
  • KEYDB updates — download, verify, save from any HTTP URL (zero deps, raw TCP)
  • Content reading — adaptive batch reads with automatic decryption
  • Stream I/O — unified stream pipeline for reading and writing any format

Streams

Stream Input Output Transport
DiscStream Yes -- Optical drive via SCSI
IsoStream Yes -- Blu-ray ISO image file (read via stream pipeline; written by freemkv_engine::recovery)
MkvStream Yes Yes Matroska container
M2tsStream Yes Yes BD transport stream with FMKV metadata header
NetworkStream Yes (listen) Yes (connect) TCP with FMKV metadata header
StdioStream Yes (stdin) Yes (stdout) Raw byte pipe
NullStream -- Yes Discard sink (byte counter for benchmarks)

Streams implement a single unified pes::Stream trait (re-exported as PesStream) exposing read() and write() on one type. input() / output() resolve URL strings to PES stream instances. All URLs use the scheme://path format — bare paths are rejected.

Keys

DVDs (CSS) decrypt out of the box, with no external key file needed.

Blu-rays and UHD (AACS) require a keydb.cfg at ~/.config/freemkv/keydb.cfg (or passed via ScanOptions). No AACS key material is compiled into the binary.

Architecture

Drive                  — open, identify, init, single-shot read
  ├── ScsiTransport    — SG_IO (Linux), IOKit (macOS), SPTI (Windows)
  └── unlock_bridge    — private seam to the freemkv-unlock crate
                         (firmware / AACS cert / CSS bus-auth unlockers)

Disc                   — scan titles, streams, AACS/CSS state
  ├── UDF reader       — Blu-ray UDF 2.50 with metadata partitions
  ├── MPLS parser      — playlists → titles + clips + streams
  ├── CLPI parser      — clip info → EP map → sector extents
  ├── IFO parser       — DVD title sets, PGC chains, cell addresses
  ├── Labels           — 5 BD-J format parsers (detect + parse)
  ├── AACS             — key resolution + content decryption
  ├── CSS              — DVD CSS (bus auth → player-key disc crack → known-plaintext title-key attack)
  └── KEYDB            — download + verify + save

Streams                — unified PES pipeline
  ├── PesStream        — pes::Stream: one trait, read()/write() PES frames
  ├── DiscStream       — sectors → decrypt → TS demux → PES
  ├── IsoStream        — ISO file → decrypt → TS demux → PES
  ├── MkvStream        — MKV mux/demux
  ├── M2tsStream       — BD transport stream
  ├── NetworkStream    — TCP with FMKV metadata header
  ├── StdioStream      — stdin/stdout pipe
  └── NullStream       — discard sink

See docs/ for detailed technical documentation on each module.

Error Codes

All errors are structured with numeric codes. No user-facing English text — applications format their own messages.

Range Category
E1xxx Device errors (not found, permission)
E2xxx Profile errors (unsupported drive)
E3xxx Unlock errors (failed, signature)
E4xxx SCSI errors (command failed, timeout)
E5xxx I/O errors
E6xxx Disc format errors
E7xxx AACS errors
E8xxx KEYDB update errors
E9xxx Stream / mux errors (URL, PES, ISO, pipeline, demux)

Platform Support

Platform Status Backend
Linux Supported SG_IO ioctl
macOS Supported IOKit SCSITask
Windows Supported SPTI

Contributing

Run freemkv info disc:// --share with the freemkv CLI to capture your drive's identity for contribution. Drive-unlock profiles are maintained in the freemkv-unlock repository.

License

MIT

S
Description
Internal mirror of libfreemkv 1.1-beta HEAD for private kdb builds (tag drops flate2; kdb needs HEAD symbols)
Readme MIT
13 MiB
Languages
Rust 99.6%
C 0.3%
Shell 0.1%