Refresh combined BIOS and dispatch validation against latest master

This commit is contained in:
Matthew Stanley
2026-09-11 23:39:47 -07:00
8 changed files with 285 additions and 22 deletions
+7
View File
@@ -58,11 +58,13 @@ Profiles declare these reusable methods:
| --- | --- |
| `disc_hashes` | Hash the cue's original data track before extracting. |
| `images[].method = fixed_address_files` | Whole original files placed at a verified `load_addr`. Optional duplicate-leaf checks require identical bytes. |
| `images[].method = tagged_relocated_files` | Apply a trailing two-bit ABS32/HI16/LO16/J26 relocation stream to a verified `load_addr`, retaining only the original loader's image interval. Destinations are explicit evidence, never inferred from a historical RAM capture. |
| `images[].method = psx_exe` | Read the load address and image from a PS-X EXE header; the generic extractor may split resident and overlay floors. |
| `images[].method = packed_sector_members` | Decode u32 count/offset sector descriptors across ordered payload files. Verify every member classification and requested full-payload coverage; use loader-established addresses for executable members. |
| `images[].method = sector_extent_members` | Decode `{sector offset, byte size}` pairs in an archive. Use archive-relative offsets, verify the exact count and terminator, reject gaps/overlaps, and account for every member after sector rounding. Each member needs a verified load address or an explicit exclusion. |
| `images[].method = aligned_lzss_banks` | Read aligned stored-size pairs, decode the metadata and its tagged bank members using parameterized LZSS, verify every container and bank classification, and deduplicate exact decoded images at verified destinations. External-RAM references fail extraction. |
| `checks[].method = words` | Verify loader instructions or descriptors at explicit file offsets / virtual addresses. |
| `checks[].method = tagged_relocations` | Validate a complete relocation stream, retained `image_size` and `relocation_count`, including modules whose heap placement remains unresolved and which are excluded from native production. |
| `checks[].method = pointer_strings` | Verify a pointer-indexed filename table against exact expected strings. |
| `checks[].method = bcd_extent_table` | Verify an indexed BCD-MSF/size table against an ISO file's actual extent. |
| `checks[].method = adjacent_files` | Require the named ISO extents to be sector aligned and physically adjacent, in order. |
@@ -90,6 +92,11 @@ coverage or native semantics; keep fallback and perform gameplay spot checks.
Jersey Devil uses `aligned_lzss_banks` for five renderer variants across 70 bank occurrences and `psx_exe` for five secondary executables. Its profile accounts for all 93 BZZ containers, including 23 without code banks.
Exact-BIOS resident helper recipes also publish a `.resident` preload marker.
The pipeline preserves it through consolidation and staging, verifies its BIOS
hash against the recipe, and includes its hash in the audit receipt. Omitting
that marker can leave an otherwise valid helper unused at runtime.
Current consumers:
- Tomba! USA: `TombaRecomp/aot/overlays.json`, 25 fixed-address images, including
+11 -6
View File
@@ -9,19 +9,24 @@ extern "C" {
/*
* GPU linked-list tags carry only 24 address bits. Trusted enhancements that
* need a larger primitive arena use this otherwise-unmapped 1 MiB aperture so
* need a larger primitive arena use this otherwise-unmapped 4 MiB aperture so
* their CPU pointers survive the tag truncation. Nothing is mapped until an
* enhancement explicitly allocates from it.
*/
#define PSX_MOD_GPU_DMA_APERTURE_BASE 0x00F00000u
#define PSX_MOD_GPU_DMA_APERTURE_SIZE (1u * 1024u * 1024u)
#define PSX_MOD_GPU_DMA_GUEST_BASE 0x80F00000u
#define PSX_MOD_GPU_DMA_APERTURE_BASE 0x00C00000u
#define PSX_MOD_GPU_DMA_APERTURE_SIZE (4u * 1024u * 1024u)
#define PSX_MOD_GPU_DMA_GUEST_BASE 0x80C00000u
static inline int psx_mod_gpu_dma_aperture_offset_for(
uint32_t address, uint32_t width, uint32_t used, uint32_t *offset) {
uint32_t canonical = address & 0x00FFFFFFu;
/* CPU addresses retain all physical bits after KSEG normalization. The
* BIOS at 0x1FC00000 must never alias the aperture at 0x00C00000.
* DMA callers pass an already-truncated 24-bit tag separately below. */
uint32_t canonical = address & 0x1FFFFFFFu;
uint32_t off;
if (canonical < PSX_MOD_GPU_DMA_APERTURE_BASE) return 0;
if (canonical < PSX_MOD_GPU_DMA_APERTURE_BASE ||
canonical >= 0x01000000u || used > PSX_MOD_GPU_DMA_APERTURE_SIZE)
return 0;
off = canonical - PSX_MOD_GPU_DMA_APERTURE_BASE;
if (off > used || width > used - off) return 0;
if (offset) *offset = off;
+29 -8
View File
@@ -15,29 +15,50 @@ static void check(int condition, const char *name) {
int main(void) {
uint32_t off = 0;
uint32_t base = PSX_MOD_GPU_DMA_APERTURE_BASE;
uint32_t size = PSX_MOD_GPU_DMA_APERTURE_SIZE;
uint32_t aliases[] = {0x1FC00000u, 0x9FC00000u, 0xBFC00000u,
0x1F000000u, 0x9F000000u, 0x1F801810u,
0xBF801810u, 0x1EFFFFFFu};
unsigned i;
check(psx_mod_gpu_dma_resolve_address_for(0x00F01234u, 0u) ==
(0x00F01234u & 0x001FFFFCu),
check(psx_mod_gpu_dma_resolve_address_for(base + 0x1234u, 0u) ==
((base + 0x1234u) & 0x001FFFFCu),
"unallocated aperture preserves retail 2 MiB folding");
check(psx_mod_gpu_dma_resolve_address_for(0x80F01234u, 0x20000u) ==
0x00F01234u,
check(psx_mod_gpu_dma_resolve_address_for(0x80000000u + base + 0x1234u, 0x20000u) ==
base + 0x1234u,
"allocated KSEG0 aperture pointer survives 24-bit DMA");
check(psx_mod_gpu_dma_resolve_address_for(0x00F30000u, 0x20000u) ==
(0x00F30000u & 0x001FFFFCu),
check(psx_mod_gpu_dma_resolve_address_for(base + 0x30000u, 0x20000u) ==
((base + 0x30000u) & 0x001FFFFCu),
"unallocated aperture tail remains folded");
check(psx_mod_gpu_dma_resolve_address_for(0x001ABCDEu, 0x20000u) ==
0x001ABCDCu,
"ordinary main RAM remains word-aligned and unchanged");
check(psx_mod_gpu_dma_aperture_offset_for(
0x00F1FFFCu, 4u, 0x20000u, &off) &&
base + 0x1FFFCu, 4u, 0x20000u, &off) &&
off == 0x1FFFCu,
"last allocated aperture word is accessible");
check(!psx_mod_gpu_dma_aperture_offset_for(
0x00F20000u, 4u, 0x20000u, &off),
base + 0x20000u, 4u, 0x20000u, &off),
"first unallocated aperture word is inaccessible");
check(!psx_mod_gpu_dma_aperture_offset_for(
0x00FFFFFCu, 8u, PSX_MOD_GPU_DMA_APERTURE_SIZE, &off),
"cross-boundary access is rejected");
for (i = 0; i < sizeof(aliases) / sizeof(aliases[0]); ++i)
check(!psx_mod_gpu_dma_aperture_offset_for(aliases[i], 4u, size, &off),
"CPU BIOS/expansion/MMIO addresses cannot alias DMA storage");
check(psx_mod_gpu_dma_aperture_offset_for(0xA0000000u + base, 4u, size, &off)
&& off == 0u, "KSEG1 CPU aperture alias resolves correctly");
check(psx_mod_gpu_dma_aperture_offset_for(base + size - 4u, 4u, size, &off)
&& off == size - 4u, "last word of full 4 MiB aperture is accessible");
check(!psx_mod_gpu_dma_aperture_offset_for(base - 4u, 4u, size, &off),
"word before aperture is rejected");
check(!psx_mod_gpu_dma_aperture_offset_for(base + size, 4u, size, &off),
"word after aperture is rejected");
check(!psx_mod_gpu_dma_aperture_offset_for(base, 4u, size + 4u, &off),
"invalid allocated size is rejected");
check(psx_mod_gpu_dma_resolve_address_for(base + size - 4u, size) ==
base + size - 4u, "full aperture DMA tag keeps its high address bits");
return failures ? 1 : 0;
}
+26 -7
View File
@@ -22,6 +22,7 @@ from aot_overlay_spike import extract_generic as extractor
from packed_sector_table import extract_members as extract_sector_members
from sector_extent_archive import extract_members as extract_extent_members
from aligned_lzss_banks import banks as extract_lzss_banks
from mips_tagged_relocations import parse as parse_tagged_relocations, relocate as relocate_tagged_image
FRAMEWORK = Path(__file__).resolve().parents[1]
@@ -74,7 +75,11 @@ def verify_evidence(disc, checks):
origin = number(check.get('file_offset', 0))
base = number(check.get('base', 0))
method = check['method']
if method == 'words':
if method == 'tagged_relocations':
image, relocations = parse_tagged_relocations(data)
require(len(image) == number(check['image_size']), 'Relocated image inventory changed')
require(len(relocations) == number(check['relocation_count']), 'Relocation inventory changed')
elif method == 'words':
for address, expected in check['values'].items():
offset = origin + number(address) - base
require(0 <= offset <= len(data) - 4, 'Evidence word outside source')
@@ -223,6 +228,11 @@ def positioned_sources(disc, specifications):
body = body[offset:]
elif method == 'fixed_address_files':
base = number(spec['load_addr'])
elif method == 'tagged_relocated_files':
base = number(spec['load_addr'])
require(not spec.get('verify_duplicate_names'),
'Relocated sources need individual source identities')
body, _ = relocate_tagged_image(body, base)
else:
raise ValueError(f'Unknown image method: {method}')
require(0x80000000 <= base < base + len(body) <= 0x80200000, f'{name}: image outside RAM')
@@ -339,7 +349,7 @@ def prepare(profile, disc, records, output):
if (lo, hi) == (source['base'], source['base'] + len(source['body'])):
singles[source['name']] = record
for source in sources:
if source['spec']['method'] in ('fixed_address_files', 'packed_sector_members', 'sector_extent_members', 'aligned_lzss_banks') and source['name'] not in singles:
if source['spec']['method'] in ('fixed_address_files', 'packed_sector_members', 'sector_extent_members', 'aligned_lzss_banks', 'tagged_relocated_files') and source['name'] not in singles:
record = make_fixed_record(source, disc)
singles[source['name']] = record
records.append(record)
@@ -487,9 +497,15 @@ def build(inventory, game_toml, recompiler, work, gcc, workers, project_root=Non
if destination.exists():
require(library.with_suffix('.ranges').read_bytes() == destination.with_suffix('.ranges').read_bytes(),
f'Conflicting native pair identity: {library.name}')
continue
shutil.copy2(library, destination)
shutil.copy2(library.with_suffix('.ranges'), destination.with_suffix('.ranges'))
else:
shutil.copy2(library, destination)
shutil.copy2(library.with_suffix('.ranges'), destination.with_suffix('.ranges'))
marker = library.with_suffix('.resident')
if marker.exists():
target_marker = destination.with_suffix('.resident')
require(not target_marker.exists() or marker.read_bytes() == target_marker.read_bytes(),
f'Conflicting resident metadata: {library.name}')
shutil.copy2(marker, target_marker)
return cache
@@ -500,8 +516,11 @@ def stage(cache, destination, receipt):
target.mkdir(parents=True, exist_ok=True)
expected = set()
for pair in receipt['pairs']:
for name, key in [(pair['dll'], 'dll_sha256'),
(Path(pair['dll']).with_suffix('.ranges').name, 'manifest_sha256')]:
artifacts = [(pair['dll'], 'dll_sha256'),
(Path(pair['dll']).with_suffix('.ranges').name, 'manifest_sha256')]
if 'resident_sha256' in pair:
artifacts.append((Path(pair['dll']).with_suffix('.resident').name, 'resident_sha256'))
for name, key in artifacts:
expected.add(name)
require(digest(source / name) == pair[key], f'Audited artifact changed: {name}')
shutil.copy2(source / name, target / name)
+22 -1
View File
@@ -11,6 +11,24 @@ from pathlib import Path
import sys
def resident_metadata(dll, record):
"""The compiler's preload opt-in must agree with the verified recipe."""
marker = dll.with_suffix('.resident')
resident = record.get('producer') == 'bios_resident_manifest'
assert marker.exists() == resident, f'Resident preload marker/recipe mismatch: {dll.name}'
if not resident:
return {}
body = marker.read_bytes()
document = json.loads(body)
assert document.get('schema') == 'psxrecomp bios resident shard v1', f'Invalid resident marker: {dll.name}'
bios_hash = record.get('bios_sha256', '')
assert len(bios_hash) == 64 and all(c in '0123456789abcdef' for c in bios_hash), 'Invalid resident BIOS hash'
assert document.get('bios_sha256') == bios_hash, f'Resident BIOS provenance mismatch: {dll.name}'
assert document.get('producer_name') == str(record.get('producer_name', 'BIOS resident code')), \
f'Resident producer mismatch: {dll.name}'
return dict(resident_sha256=hashlib.sha256(body).hexdigest())
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--framework-root', type=Path,
@@ -46,6 +64,7 @@ def main():
dict(name=f'record-{index:03d}', record=record)
for index, record in enumerate(document)]
recipes = []
records_by_name = {}
for job in jobs:
if args.inventory:
record = json.loads(Path(job['input']).read_text(encoding='utf-8-sig'))[0]
@@ -65,6 +84,7 @@ def main():
bounds = [(lo & 0x1fffffff, hi & 0x1fffffff) for lo, hi in known]
assert all(lo < hi for lo, hi in bounds)
recipes.append((job['name'], load, data, bounds))
records_by_name[job['name']] = record
pairs = []
native_ids = []
@@ -89,7 +109,8 @@ def main():
assert matched, f'No original-input guard proof for {dll.name}'
pairs.append(dict(dll=dll.name, functions=len(ids), matched_recipe=matched,
dll_sha256=hashlib.sha256(dll.read_bytes()).hexdigest(),
manifest_sha256=hashlib.sha256(dll.with_suffix('.ranges').read_bytes()).hexdigest()))
manifest_sha256=hashlib.sha256(dll.with_suffix('.ranges').read_bytes()).hexdigest(),
**resident_metadata(dll, records_by_name[matched])))
if args.expected_pairs is not None:
assert len(pairs) == args.expected_pairs, (len(pairs), args.expected_pairs)
image_coverage = []
+75
View File
@@ -0,0 +1,75 @@
"""Reconstruct MIPS images with a trailing two-bit tagged relocation stream.
The first little-endian word is the byte offset of the stream and therefore
the retained image size. Stream words encode an aligned image offset plus:
0 = ABS32, 1 = HI16 with a following full addend, 2 = LO16, 3 = J26.
0xffffffff ends the stream. This format carries no load address: callers must
establish each destination independently from the original loader.
"""
from dataclasses import dataclass
import struct
@dataclass(frozen=True)
class Relocation:
offset: int
kind: int
addend: int | None = None
def parse(data: bytes) -> tuple[bytes, tuple[Relocation, ...]]:
"""Validate the entire file and return its retained image and relocations."""
if len(data) < 8 or len(data) % 4:
raise ValueError("Relocation file must contain aligned header and terminator")
end = struct.unpack_from("<I", data)[0]
if end < 8 or end % 4 or end > len(data) - 4:
raise ValueError("Invalid relocation stream offset")
cursor, relocations, written = end, [], set()
while cursor <= len(data) - 4:
tag = struct.unpack_from("<I", data, cursor)[0]
cursor += 4
if tag == 0xFFFFFFFF:
if cursor != len(data):
raise ValueError("Unaccounted bytes after relocation terminator")
return data[:end], tuple(relocations)
offset, kind, addend = tag & ~3, tag & 3, None
if offset < 4 or offset + 4 > end:
raise ValueError("Relocation target outside retained image")
if offset in written:
raise ValueError("Repeated relocation target")
written.add(offset)
if kind == 1:
if cursor > len(data) - 4:
raise ValueError("Truncated HI16 addend")
addend = struct.unpack_from("<I", data, cursor)[0]
cursor += 4
relocations.append(Relocation(offset, kind, addend))
raise ValueError("Missing relocation terminator")
def relocate(data: bytes, load_addr: int) -> tuple[bytes, tuple[Relocation, ...]]:
"""Apply the loader's 32-bit and halfword writes at a verified RAM base."""
source, relocations = parse(data)
if load_addr % 4 or not 0x80000000 <= load_addr < load_addr + len(source) <= 0x80200000:
raise ValueError("Relocated image must fit aligned PlayStation main RAM")
image = bytearray(source)
for item in relocations:
offset = item.offset
if item.kind == 0:
word = struct.unpack_from("<I", image, offset)[0]
struct.pack_into("<I", image, offset, (word + load_addr) & 0xFFFFFFFF)
elif item.kind == 1:
# The original loader adds 0x8000 before extracting the high word
# because the matching low-half instruction sign-extends its imm.
value = ((load_addr + item.addend + 0x8000) & 0xFFFFFFFF) >> 16
struct.pack_into("<H", image, offset, value)
elif item.kind == 2:
value = struct.unpack_from("<H", image, offset)[0]
struct.pack_into("<H", image, offset, (value + load_addr) & 0xFFFF)
else:
word = struct.unpack_from("<I", image, offset)[0]
# This is addition to the complete instruction word, matching the
# loader, rather than masking or replacing the encoded jump field.
value = word + (((load_addr << 4) & 0xFFFFFFFF) >> 6)
struct.pack_into("<I", image, offset, value & 0xFFFFFFFF)
return bytes(image), relocations
+68
View File
@@ -11,6 +11,7 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import aot_overlay_pipeline as pipeline
import audit_aot_cache as auditor
class FakeDisc:
@@ -23,6 +24,73 @@ class FakeDisc:
class AotMethodsTest(unittest.TestCase):
def test_tagged_relocated_original_file_and_inventory_check(self):
original = struct.pack('<8I', 16, 8, 0x03e00008, 0, 4, 0xFFFFFFFF, 0, 0)[:24]
disc = FakeDisc({'MODULE.DLL': original})
spec = dict(method='tagged_relocated_files', files=['MODULE.DLL'],
load_addr='0x80100000', allow_missing=True, entries=['0x80100008'])
source, = pipeline.positioned_sources(disc, [spec])
self.assertEqual(source['body'], struct.pack('<4I', 16, 0x80100008, 0x03e00008, 0))
self.assertEqual(disc.read('MODULE.DLL'), original)
check = dict(method='tagged_relocations', file='MODULE.DLL', image_size=16, relocation_count=1)
pipeline.verify_evidence(disc, [check])
with self.assertRaisesRegex(ValueError, 'inventory changed'):
pipeline.verify_evidence(disc, [{**check, 'relocation_count': 2}])
with tempfile.TemporaryDirectory() as directory:
disc.binary = Path(directory) / 'source.bin'
disc.binary.write_bytes(original)
inventory = pipeline.prepare(dict(game_id='TEST', images=[spec], checks=[check],
expected_records=1, strict_bounds=True), disc, [], Path(directory))
self.assertEqual(inventory['required_images'], ['MODULE.DLL'])
self.assertEqual(inventory['jobs'][0]['required_entries'], [0x80100008])
def test_resident_preload_metadata_survives_build_audit_and_stage(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
record = dict(producer='bios_resident_manifest', bios_sha256='a' * 64,
producer_name='Synthetic exact-BIOS helper')
filename = '00001000_12345678' + pipeline.compiler.overlay_ext()
def compile_fixture(command, **kwargs):
cache = Path(command[command.index('--out-dir') + 1])
leaf = cache / 'TEST/gcc' / pipeline.compiler.cache_arch_abi() / 'cg'
leaf.mkdir(parents=True)
dll = leaf / filename
dll.write_bytes(b'Synthetic library')
dll.with_suffix('.ranges').write_bytes(b'Synthetic ranges')
pipeline.compiler.update_bios_resident_marker(str(dll), record)
with mock.patch.object(pipeline.subprocess, 'run', side_effect=compile_fixture):
cache = pipeline.build(dict(jobs=[dict(input='input.json', name='resident')]),
root / 'game.toml', root / 'emitter', root, 'gcc', 1)
library = next(cache.rglob('*' + pipeline.compiler.overlay_ext()))
pair = dict(dll=filename, dll_sha256=pipeline.digest(library),
manifest_sha256=pipeline.digest(library.with_suffix('.ranges')),
**auditor.resident_metadata(library, record))
receipt = dict(game_id='TEST', cache_tag='cg', pairs=[pair])
pipeline.stage(cache, root / 'stage', receipt)
staged = next((root / 'stage').rglob('*.resident'))
self.assertEqual(staged.read_bytes(), library.with_suffix('.resident').read_bytes())
library.with_suffix('.resident').write_bytes(b'changed after audit')
with self.assertRaisesRegex(ValueError, 'Audited artifact changed'):
pipeline.stage(cache, root / 'stage', receipt)
def test_resident_audit_rejects_missing_unproven_and_changed_markers(self):
with tempfile.TemporaryDirectory() as directory:
dll = Path(directory) / ('helper' + pipeline.compiler.overlay_ext())
record = dict(producer='bios_resident_manifest', bios_sha256='a' * 64)
with self.assertRaisesRegex(AssertionError, 'marker/recipe mismatch'):
auditor.resident_metadata(dll, record)
pipeline.compiler.update_bios_resident_marker(str(dll), record)
self.assertIn('resident_sha256', auditor.resident_metadata(dll, record))
with self.assertRaisesRegex(AssertionError, 'marker/recipe mismatch'):
auditor.resident_metadata(dll, {})
with self.assertRaisesRegex(AssertionError, 'BIOS provenance'):
auditor.resident_metadata(dll, {**record, 'bios_sha256': 'b' * 64})
marker = dll.with_suffix('.resident')
document = json.loads(marker.read_bytes())
marker.write_text(json.dumps({**document, 'schema': 'unknown'}), encoding='utf-8')
with self.assertRaisesRegex(AssertionError, 'Invalid resident marker'):
auditor.resident_metadata(dll, record)
def test_release_refuses_config_that_would_ignore_bundled_native_modules(self):
for config in ({}, {'runtime': {}}, {'runtime': {'overlay_cache': False}}):
with self.subTest(config=config), self.assertRaisesRegex(ValueError, 'overlay_cache = true'):
@@ -0,0 +1,47 @@
"""Synthetic exact-loader and malformed-input checks; no game bytes."""
from pathlib import Path
import struct
import sys
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from mips_tagged_relocations import parse, relocate
def packed(words):
return struct.pack("<" + "I" * len(words), *words)
class TaggedRelocationTests(unittest.TestCase):
def test_all_four_writes_and_hi16_carry(self):
data = packed([24, 0x28, 0x3C080000, 0x2508F000, 0x0C00000A,
0xDEADBEEF, 4, 8 | 1, 0xF000, 12 | 2, 16 | 3, 0xFFFFFFFF])
image, records = relocate(data, 0x80108000)
self.assertEqual(struct.unpack("<6I", image),
(24, 0x80108028, 0x3C088011, 0x25087000, 0x0C04200A, 0xDEADBEEF))
self.assertEqual([r.kind for r in records], [0, 1, 2, 3])
self.assertEqual(len(data), 48)
def test_no_relocations(self):
image, records = relocate(packed([8, 0x12345678, 0xFFFFFFFF]), 0x80100000)
self.assertEqual(image, packed([8, 0x12345678]))
self.assertEqual(records, ())
def test_rejects_malformed_streams(self):
invalid = [b"", packed([8, 0]), packed([7, 0, 0xFFFFFFFF]),
packed([8, 0, 0]), packed([8, 0, 8, 0xFFFFFFFF]),
packed([8, 0, 5]), packed([8, 0, 4, 4, 0xFFFFFFFF]),
packed([8, 0, 0xFFFFFFFF, 0])]
for data in invalid:
with self.subTest(data=data), self.assertRaises(ValueError):
parse(data)
def test_rejects_unproven_memory_layout(self):
data = packed([8, 0, 0xFFFFFFFF])
for base in [0, 0x80000002, 0x80200000, 0x801FFFFC]:
with self.subTest(base=base), self.assertRaises(ValueError):
relocate(data, base)
if __name__ == "__main__":
unittest.main()