disc/bdnav: audit round-1 — VM title/PSR faithfulness + main_title never-panics

From the /codeaudit HEAD..v1.6.10 round (each verified against the libbluray
HDMV VM reference, and design-refuted SOUND on two models):

- bdnav/vm.rs title_obj: resolve First Play via the spec sentinel 0xFFFF (was
  num_titles+1). A JumpTitle 0xFFFF now reaches First Play, and an out-of-range
  JumpTitle(N+1) correctly abstains instead of mis-jumping there.
- bdnav/vm.rs wr: refuse stores to PSR-tagged registers (matches _store_reg),
  so an illegal SET/SWAP to a PSR is a no-op and later compares agree with a
  real player instead of diverging.
- disc/profile.rs main_title(): return Option<&TitleProfile> instead of indexing
  titles[0], which panicked on a disc that scanned to zero titles.

Regression tests added, each shown red-before-green.
This commit is contained in:
Matthew Jackson
2026-08-25 18:16:17 -07:00
parent b405154fd0
commit 3ecbeb7eba
2 changed files with 75 additions and 10 deletions
+59 -5
View File
@@ -76,9 +76,10 @@ impl Vm<'_> {
} }
} }
fn wr(&mut self, val: u32, x: u32) { fn wr(&mut self, val: u32, x: u32) {
if val & PSR_FLAG != 0 { // A store to a PSR-tagged register is refused (libbluray `_store_reg`
self.psr[(val & 0x7f) as usize] = x; // returns -1); only GPR writes take effect. PSR values come from the
} else { // system (power-on `psr_init`), never from a nav SET/SWAP operand.
if val & PSR_FLAG == 0 {
self.gpr[(val & 0xfff) as usize] = x; self.gpr[(val & 0xfff) as usize] = x;
} }
} }
@@ -86,14 +87,16 @@ impl Vm<'_> {
if imm { raw } else { self.rd(raw) } if imm { raw } else { self.rd(raw) }
} }
/// Resolve a JumpTitle target: title 0 = Top Menu, `1..=N` = `titles[i-1]`, /// Resolve a JumpTitle target: title 0 = Top Menu, `1..=N` = `titles[i-1]`,
/// `N+1` = First Play (BD spec title numbering). /// `0xFFFF` = First Play (BD-ROM title numbering, per libbluray
/// `_is_valid_title`). Any other value is an invalid title reference and
/// abstains (`None`).
fn title_obj(&self, title: u32) -> Option<PlaybackObj> { fn title_obj(&self, title: u32) -> Option<PlaybackObj> {
let n = self.index.titles.len() as u32; let n = self.index.titles.len() as u32;
if title == 0 { if title == 0 {
Some(self.index.top_menu) Some(self.index.top_menu)
} else if title <= n { } else if title <= n {
self.index.titles.get((title - 1) as usize).copied() self.index.titles.get((title - 1) as usize).copied()
} else if title == n + 1 { } else if title == 0xFFFF {
Some(self.index.first_play) Some(self.index.first_play)
} else { } else {
None None
@@ -336,6 +339,57 @@ mod tests {
assert_eq!(resolve(&index, &mobjs, &|_| true), None); assert_eq!(resolve(&index, &mobjs, &|_| true), None);
} }
#[test]
fn jumptitle_first_play_is_0xffff_not_n_plus_1() {
// BD-ROM title numbering (libbluray `_is_valid_title`): 0 = Top Menu,
// 0xFFFF = First Play, 1..=N = titles. `N+1` is an INVALID title ref.
let index = idx(
PlaybackObj::Hdmv { id_ref: 7 },
vec![PlaybackObj::Hdmv { id_ref: 1 }],
);
let vm = Vm {
mobjs: &[],
index: &index,
gpr: [0; 4096],
psr: psr_init(),
};
assert_eq!(vm.title_obj(0), Some(index.top_menu), "title 0 = Top Menu");
assert_eq!(
vm.title_obj(1),
Some(PlaybackObj::Hdmv { id_ref: 1 }),
"title 1 = titles[0]"
);
assert_eq!(
vm.title_obj(0xFFFF),
Some(PlaybackObj::Hdmv { id_ref: 7 }),
"0xFFFF = First Play (abstained wrongly before the fix)"
);
assert_eq!(
vm.title_obj(2),
None,
"N+1 is an invalid title ref, not First Play (resolved wrongly before the fix)"
);
}
#[test]
fn set_to_psr_is_refused() {
// A SET MOVE to PSR6 must be a no-op (libbluray `_store_reg` refuses PSR
// stores). Program: MOVE PSR6 <- 5; CMP PSR6 == 5; PlayPL 100 else 200.
// With the write refused, PSR6 stays at its power-on 0, so the compare is
// false and control skips to PlayPL 200. If the illegal PSR write took
// effect, the compare would be true and PlayPL 100 would win.
let set_psr6 = cmd((2 << 5) | (2 << 3), 0x40, 0, 0x01, 0x8000_0006, 5);
let cmp_psr6_eq_5 = cmd((2 << 5) | (1 << 3), 0x40, 0x02, 0, 0x8000_0006, 5);
let d = build(&[&[set_psr6, cmp_psr6_eq_5, play_pl(100), play_pl(200)]]);
let mobjs = mobj::parse(&d).unwrap();
let index = idx(PlaybackObj::Hdmv { id_ref: 0 }, vec![]);
assert_eq!(
resolve(&index, &mobjs, &|id| id == 100 || id == 200),
Some(200),
"PSR store must be refused, so the compare is false and 200 is reached"
);
}
#[test] #[test]
fn register_fold_and_cmp_skip_reach_playpl() { fn register_fold_and_cmp_skip_reach_playpl() {
// SET GPR[3]=7; CMP GPR[3]==7 (true → do NOT skip); PlayPL GPR[3]. // SET GPR[3]=7; CMP GPR[3]==7 (true → do NOT skip); PlayPL GPR[3].
+16 -5
View File
@@ -268,10 +268,12 @@ impl DiscProfile {
} }
} }
/// The selected main-feature title. Panics only on an empty disc (no /// The selected main-feature title, or `None` when the disc scanned to zero
/// titles), which the scan never produces for a real disc. /// titles (a data-only image with no /BDMV, /HVDVD_TS or /VIDEO_TS). Never
pub fn main_title(&self) -> &TitleProfile { /// panics — mirrors the `Option`-returning main-feature accessors elsewhere
&self.titles[self.main_title] /// (`titles.first()`, `dvdnav::resolve_main_title`).
pub fn main_title(&self) -> Option<&TitleProfile> {
self.titles.get(self.main_title)
} }
} }
@@ -316,6 +318,15 @@ mod tests {
} }
} }
#[test]
fn main_title_is_none_on_empty_disc() {
// A data-only image (no /BDMV, /HVDVD_TS, /VIDEO_TS) scans to zero
// titles; main_title() must return None, not panic on titles[0].
let profile = DiscProfile::from_disc(&test_disc());
assert!(profile.titles.is_empty());
assert!(profile.main_title().is_none());
}
fn video(codec: Codec, res: Resolution, secondary: bool) -> Stream { fn video(codec: Codec, res: Resolution, secondary: bool) -> Stream {
Stream::Video(VideoStream { Stream::Video(VideoStream {
pid: 0x1011, pid: 0x1011,
@@ -469,7 +480,7 @@ mod tests {
assert!(profile.titles[0].is_main, "titles[0] is the main feature"); assert!(profile.titles[0].is_main, "titles[0] is the main feature");
assert!(!profile.titles[1].is_main); assert!(!profile.titles[1].is_main);
assert_eq!(profile.titles[1].index, 1); assert_eq!(profile.titles[1].index, 1);
assert_eq!(profile.main_title().playlist, "00800.mpls"); assert_eq!(profile.main_title().unwrap().playlist, "00800.mpls");
} }
#[test] #[test]