aacs: a Stop while reading index.bdmv is Halted

The stride resolver's index.bdmv read, and the live UHD check, took a Stop for an unreadable
index and went on with a default stride. The capture and the scan's version read now end as
Halted, like their other reads.
This commit is contained in:
Matthew Jackson
2026-10-04 12:25:45 -07:00
parent ff96202a22
commit a27431d962
2 changed files with 84 additions and 29 deletions
+64 -18
View File
@@ -128,27 +128,35 @@ pub(crate) fn handshake_class_error(e: &Error) -> Option<Error> {
} }
// `Some(true)` for an `INDX0300` index.bdmv, `Some(false)` for another INDX version, `None` // `Some(true)` for an `INDX0300` index.bdmv, `Some(false)` for another INDX version, `None`
// when there is no readable index (HD DVD, damage). // when there is no readable index (HD DVD, damage). `Err` only for a Stop.
pub(super) fn index_is_uhd(udf_fs: &udf::UdfFs, reader: &mut dyn SectorSource) -> Option<bool> { pub(super) fn index_is_uhd(
let index = udf_fs udf_fs: &udf::UdfFs,
.read_file_prefix(reader, "/BDMV/index.bdmv", 8) reader: &mut dyn SectorSource,
.ok()?; ) -> Result<Option<bool>> {
let version = index.get(..8)?.strip_prefix(b"INDX")?; let index = match udf_fs.read_file_prefix(reader, "/BDMV/index.bdmv", 8) {
Some(version == b"0300") Ok(index) => index,
Err(Error::Halted) => return Err(Error::Halted),
Err(_) => return Ok(None),
};
let version = index.get(..8).and_then(|v| v.strip_prefix(b"INDX"));
Ok(version.map(|v| v == b"0300"))
} }
// Positive UHD evidence independent of the cert: index.bdmv version "0300" // Positive UHD evidence independent of the cert: index.bdmv version "0300"
// (BD-ROM Part 3) or an AACS2 MKB. Unknown is `false`. // (BD-ROM Part 3) or an AACS2 MKB. Unknown is `false`; `Err` only for a Stop.
fn disc_is_uhd(udf_fs: &udf::UdfFs, reader: &mut dyn SectorSource) -> bool { fn disc_is_uhd(udf_fs: &udf::UdfFs, reader: &mut dyn SectorSource) -> Result<bool> {
use crate::aacs::mkb::{AacsVersion, mkb_type}; use crate::aacs::mkb::{AacsVersion, mkb_type};
if index_is_uhd(udf_fs, reader) == Some(true) { if index_is_uhd(udf_fs, reader)? == Some(true) {
return true; return Ok(true);
} }
udf_fs let mkb = match udf_fs.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, 64) {
.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, 64) Ok(mkb) => mkb,
.ok() Err(Error::Halted) => return Err(Error::Halted),
.and_then(|m| mkb_type(&m).map(|t| t.generation())) Err(_) => return Ok(false),
.is_some_and(|g| matches!(g, AacsVersion::V20 | AacsVersion::V21)) };
Ok(mkb_type(&mkb)
.map(|t| t.generation())
.is_some_and(|g| matches!(g, AacsVersion::V20 | AacsVersion::V21)))
} }
/// A disc's AACS files, read before any AACS command is sent to the drive. /// A disc's AACS files, read before any AACS command is sent to the drive.
@@ -217,7 +225,7 @@ pub(super) fn capture(
// Fail safe: an unparseable cert, or none on a live disc known to be UHD, may mean bus encryption. // Fail safe: an unparseable cert, or none on a live disc known to be UHD, may mean bus encryption.
let bus_encryption = match &cc { let bus_encryption = match &cc {
Some(c) => c.bus_encryption, Some(c) => c.bus_encryption,
None => cc_raw.is_some() || (live && disc_is_uhd(udf_fs, reader)), None => cc_raw.is_some() || (live && disc_is_uhd(udf_fs, reader)?),
}; };
// The bounded MKB reader, not `read_file` (the ~128 MiB padded MKB would fail). // The bounded MKB reader, not `read_file` (the ~128 MiB padded MKB would fail).
let mkb = match Disc::read_mkb_content(reader, udf_fs) { let mkb = match Disc::read_mkb_content(reader, udf_fs) {
@@ -235,7 +243,7 @@ pub(super) fn capture(
}; };
// Stride version through the shared resolver (cert, then MKB type, then index.bdmv), // Stride version through the shared resolver (cert, then MKB type, then index.bdmv),
// the same one `read_aacs_version` uses. // the same one `read_aacs_version` uses.
let index = index_is_uhd(udf_fs, reader); let index = index_is_uhd(udf_fs, reader)?;
let version = let version =
aacs::mkb::resolve_aacs_version(cc.as_ref().map(|c| c.version.major()), &mkb, index) aacs::mkb::resolve_aacs_version(cc.as_ref().map(|c| c.version.major()), &mkb, index)
.major(); .major();
@@ -774,6 +782,44 @@ mod tests {
state.ok_or_else(|| err.unwrap_or(Error::AacsNoKeys)) state.ok_or_else(|| err.unwrap_or(Error::AacsNoKeys))
} }
// A Stop while reading index.bdmv (the stride's last resort, and the UHD check of a live
// disc with no cert) ends the capture as Halted, not as an unknown index.
#[test]
fn a_stop_reading_the_index_is_halted() {
struct StopAt<'a>(&'a mut MemDisc, u32);
impl SectorSource for StopAt<'_> {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
if lba == self.1 {
return Err(Error::Halted);
}
self.0.read_sectors(lba, count, buf, recovery)
}
}
for from in [CaptureFrom::Image, CaptureFrom::Live] {
let mut disc = MemDisc::new();
let uk = AacsFile {
name: "Unit_Key_RO.inf",
icb_lba: 60,
data_lba: 5000,
contents: vec![0xAB; 32],
};
let udf = build_aacs_fs_with_index(&mut disc, &[uk], Some(b"INDX0300"));
let mut reader = StopAt(&mut disc, PART_START + 7000);
let cap = capture(&mut reader, &udf, from);
assert!(
matches!(cap, Err(Error::Halted)),
"live: {}",
from == CaptureFrom::Live
);
}
}
/// Missing Unit_Key_RO.inf (and its DUPLICATE) → Error::AacsNoKeys /// Missing Unit_Key_RO.inf (and its DUPLICATE) → Error::AacsNoKeys
/// (encrypt.rs `.map_err(|_| Error::AacsNoKeys)`). Never panics. /// (encrypt.rs `.map_err(|_| Error::AacsNoKeys)`). Never panics.
#[test] #[test]
+20 -11
View File
@@ -2251,21 +2251,30 @@ impl Disc {
|p| udf_fs.read_file(reader, p), |p| udf_fs.read_file(reader, p),
)?; )?;
let mkb = Self::read_mkb_content(reader, udf_fs)?; let mkb = Self::read_mkb_content(reader, udf_fs)?;
let version = Self::read_aacs_version(reader, udf_fs, &mkb); let version = Self::read_aacs_version(reader, udf_fs, &mkb)?;
Ok((inf, mkb, version)) Ok((inf, mkb, version))
} }
// AACS major version driving the Unit_Key_RO.inf parse stride: the content certificate, // AACS major version driving the Unit_Key_RO.inf parse stride: the content certificate,
// then the MKB type, then index.bdmv, through the shared `resolve_aacs_version`. // then the MKB type, then index.bdmv, through the shared `resolve_aacs_version`. `Err` only
fn read_aacs_version(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs, mkb: &[u8]) -> u8 { // for a Stop.
let cert_major = crate::aacs::read_first( fn read_aacs_version(
reader: &mut dyn SectorSource,
udf_fs: &udf::UdfFs,
mkb: &[u8],
) -> Result<u8> {
let cert = crate::aacs::read_first(
&crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::ContentCert), &crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::ContentCert),
|p| udf_fs.read_file(reader, p), |p| udf_fs.read_file(reader, p),
) );
.ok() if matches!(cert, Err(Error::Halted)) {
.as_deref() return Err(Error::Halted);
.and_then(crate::aacs::inf::parse_content_cert) }
.map(|c| c.version.major()); let cert_major = cert
.ok()
.as_deref()
.and_then(crate::aacs::inf::parse_content_cert)
.map(|c| c.version.major());
if cert_major.is_none() { if cert_major.is_none() {
tracing::warn!( tracing::warn!(
target: "freemkv::disc", target: "freemkv::disc",
@@ -2274,8 +2283,8 @@ impl Disc {
or index.bdmv" or index.bdmv"
); );
} }
let index = encrypt::index_is_uhd(udf_fs, reader); let index = encrypt::index_is_uhd(udf_fs, reader)?;
crate::aacs::mkb::resolve_aacs_version(cert_major, mkb, index).major() Ok(crate::aacs::mkb::resolve_aacs_version(cert_major, mkb, index).major())
} }
// Reads the AACS MKB's real record stream — NOT its ~128 MiB zero padding. Reads a bounded, // Reads the AACS MKB's real record stream — NOT its ~128 MiB zero padding. Reads a bounded,