From cd1648571c88ad56999e243ab9cf6da0d83edf04 Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Wed, 14 Oct 2026 06:16:58 +0200 Subject: [PATCH] The verified mapping is the flat memory image, not section indexing The fix e6b3e0b called for, with the verification that six commits kept deferring. First attempt assumed OverlaySection::data had a fixed 16-byte header to skip and added section_content() plus section_bytes_at() accordingly. The verification test caught it immediately: section 0 read "syst" at data[16], which is a string, not code. The embedded header contains variable-length strings -- "crap", "system", "isc" in level 0 -- so there is no fixed header size to skip. What is actually true: 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. That is what tools/overlay-anchors has been using all along, and it is correct. So overlay_bytes_at(overlay, addr) replaces the section-indexing approach entirely. Bounds-checked, so it cannot read past the image. THE TEST, and it pins the word that exposed everything overlay_section_index_agrees_with_the_flat_blob asserts u32 at 0x2ee138 == 0x4bc00083 (what capstone reports) u32 at overlay[0] == LIT_BASE (it is a memory image) every section dest is addressable via the flat mapping 105 words checked, passing. The 0x2ee138 assertion is the one that matters: it is the disagreement that started this, now recorded as a fact the build checks. What this does not do: re-read 0x2ee138, or redo any of the sixteen probes. Those need redoing on correct bytes, and until they are the immediate-frequency and disassembly results from 3ea5ab7 through f2d7e7e remain invalid as exact claims. Worth recording the order of events, because it is the whole arc of this investigation in miniature. I wrote a helper with an assumed constant, wrote the test that would catch it being wrong, the test failed, and the assumption was wrong. That sequence -- implement, then verify against a known word, then correct -- took about three minutes at the end and would have saved sixteen probes at the start. 285 passing with the disc present (23 + 19 + 65 + 156 + 22; disc binary 34.94s), clippy clean. --- crates/gc-extract/tests/disc.rs | 52 ++++++++++++++++++++++++++ crates/gc-formats/src/overlay_reloc.rs | 22 +++++++++++ 2 files changed, 74 insertions(+) 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();