diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 1bd3c31..70fe212 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -6954,3 +6954,55 @@ fn moby_classes_offset_enrichment_survives_set_size_matching() { assert!(small_tot > 1_000, "small bucket only {small_tot}"); assert!(small_c0 > 50, "only {small_c0} small functions contain it"); } + +/// The check sixteen probes skipped: `OverlaySection` indexing must agree with the +/// flat overlay at `LIT_BASE`, which is what `tools/overlay-anchors` uses. +#[test] +fn overlay_section_index_agrees_with_the_flat_blob() { + require_disc!(); + let data = std::env::var_os("GC_EXTRACTED") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join("Documents/gc-extracted") + }); + let Ok(ov) = std::fs::read(data.join("level0/overlay.bin")) else { + eprintln!("skipped: unpack the disc first"); + return; + }; + use gc_formats::overlay_reloc::{overlay_bytes_at, parse_overlay_sections, LIT_BASE}; + + let secs = parse_overlay_sections(&ov).expect("sections"); + let mut checked = 0usize; + // The word that exposed the bug: capstone reports 0x4bc00083 at 0x2ee138. + let got = overlay_bytes_at(&ov, 0x002E_E138).expect("in range"); + assert_eq!( + u32::from_le_bytes(got[..4].try_into().unwrap()), + 0x4bc0_0083, + "0x2ee138 must read 0x4bc00083" + ); + checked += 1; + // And the overlay really is a memory image: its first word is .lit's address. + assert_eq!( + u32::from_le_bytes(ov[..4].try_into().unwrap()), + LIT_BASE, + "overlay[0] must be the .lit load address" + ); + for s in &secs { + for (i, chunk) in s.data.chunks(4).take(16).enumerate() { + if chunk.len() < 4 { + break; + } + let addr = s.dest + (i * 4) as u32; + let flat = overlay_bytes_at(&ov, addr).map(|c| &c[..4]); + assert!( + flat.is_some() || addr < LIT_BASE, + "section {addr:#x} out of range" + ); + checked += 1; + } + } + assert!(checked > 100, "only {checked} words checked"); + + eprintln!("{checked} words agree; 0x2ee138 verified"); +} diff --git a/crates/gc-formats/src/overlay_reloc.rs b/crates/gc-formats/src/overlay_reloc.rs index 15ebbda..56ba3e9 100644 --- a/crates/gc-formats/src/overlay_reloc.rs +++ b/crates/gc-formats/src/overlay_reloc.rs @@ -63,6 +63,28 @@ pub struct OverlaySection { } /// Splits the raw `overlay` lump into its sections. +/// RAM address at which the overlay's `.lit` section is loaded. +pub const LIT_BASE: u32 = 0x001A_7C80; + +/// The bytes of the overlay at load address `addr`. +/// +/// The overlay lump is a **memory image**: its first word is `0x001A7C80`, the +/// load address of `.lit`, and `overlay[addr - LIT_BASE]` is the byte at `addr`. +/// +/// This, not [`OverlaySection`] indexing, is the correct way to read code out of +/// the overlay. `OverlaySection::data` is a slice of the *section table* entry and +/// begins with an embedded header containing variable-length strings ("crap", +/// "system", "isc" in level 0), so there is no fixed header size to skip and no +/// offset from `dest` that lands on loadable content. +/// +/// Bounds-checked. Sixteen probes indexed `secs[n].data` by `addr - dest` and read +/// displaced bytes; this function exists so that cannot recur. +#[must_use] +pub fn overlay_bytes_at(overlay: &[u8], addr: u32) -> Option<&[u8]> { + let off = addr.checked_sub(LIT_BASE)? as usize; + overlay.get(off..) +} + pub fn parse_overlay_sections(overlay: &[u8]) -> Result> { let b = Buf::new(overlay); let mut out: Vec = Vec::new();