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.
This commit is contained in:
2026-10-14 06:16:58 +02:00
parent e6b3e0b091
commit cd1648571c
2 changed files with 74 additions and 0 deletions
+52
View File
@@ -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");
}
+22
View File
@@ -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<Vec<OverlaySection>> {
let b = Buf::new(overlay);
let mut out: Vec<OverlaySection> = Vec::new();