From 62983c9a8a6045e6cf7cd0963224dee782e6029d Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Wed, 23 Sep 2026 23:11:38 -0400 Subject: [PATCH] phase8: syscall returns, so it is not a walk terminator Worker C found that config/function_extents.tsv mis-split the two BIOS stubs: it recorded 0x80103FCC..0x80103FD4 (8 bytes) and 0x80103FEC..0x80103FF4, but the real bodies are 16 bytes (li a0,N / syscall / jr ra / nop). Its evidence: the first address has four jal callers and the following address has none, syscall returns to EPC+4 so the body must continue to a jr ra, every neighbouring stub is 16 bytes with the same shape, and an 8-byte body is unreachable from C because cc1 always appends an epilogue. Root cause: the walk treated `syscall` as a terminal alongside `break`. On this target syscall is the BIOS call instruction and it returns to the next word. `break` still terminates. After the fix both stubs are 16 bytes ending at their jr ra, and all 115 registered regions still agree with their derived extents. The fix also exposed three walks in the 0x8017xxxx region that the accidental syscall stop had been bounding: 0x8017D5C0 now runs out of the payload (grade=outside), 0x801800C4 becomes a 1860-byte fallthrough, and 0x8018080C a 240708-byte indirect. All three are jal-graded starts whose bodies are almost certainly data; none is registered and none is matchable. 200 tests pass. --- config/function_extents.tsv | 10 +++++----- tools/sf3_extents | 24 +++++++++++++++++------- tools/tests/test_sf3_extents.py | 18 +++++++++++++++--- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/config/function_extents.tsv b/config/function_extents.tsv index c612ac3..1f42ef0 100644 --- a/config/function_extents.tsv +++ b/config/function_extents.tsv @@ -2263,9 +2263,9 @@ 0x80103D60 - - - - standalone grades=prologue 0x80103F84 0x80103FA8 36 0x80103FB4 12 exact term=jr_ra 0x80103FB4 0x80103FCC 24 0x80103FCC 0 indirect jr_reg=0x80103FB8;lower_bound=0x80103FC0 -0x80103FCC 0x80103FD4 8 0x80103FDC 8 exact term=syscall +0x80103FCC 0x80103FDC 16 0x80103FDC 0 exact term=jr_ra 0x80103FDC 0x80103FEC 16 0x80103FEC 0 indirect jr_reg=0x80103FE0;lower_bound=0x80103FE8 -0x80103FEC 0x80103FF4 8 0x80103FFC 8 exact term=syscall +0x80103FEC 0x80103FFC 16 0x80103FFC 0 exact term=jr_ra 0x80103FFC 0x8010400C 16 0x8010400C 0 indirect jr_reg=0x80104000;lower_bound=0x80104008 0x8010400C 0x80104038 44 0x80104230 504 exact term=jr_ra 0x80104040 - - - - standalone grades=prologue @@ -2664,10 +2664,10 @@ 0x8016E504 0x8016E60C 264 0x8016E60C 0 fallthrough hit=0x8016E60C 0x8016E60C 0x8016E93C 816 0x8016E93C 0 fallthrough hit=0x8016E93C 0x8016E93C 0x8017D5C0 60548 0x8017D5C0 0 indirect jr_reg=0x8017075C;lower_bound=0x80170764 -0x8017D5C0 0x8017D75C 412 0x801800C4 10600 exact term=syscall -0x801800C4 0x801801B8 244 0x80180808 1616 exact term=syscall +0x8017D5C0 0x801800C4 11012 0x801800C4 0 outside pc=0x8FFC23FC +0x801800C4 0x80180808 1860 0x80180808 0 fallthrough hit=0x80180808 0x80180808 0x8018080C 4 0x8018080C 0 fallthrough hit=0x8018080C -0x8018080C 0x80180984 376 0x801BB450 240332 exact term=syscall +0x8018080C 0x801BB450 240708 0x801BB450 0 indirect jr_reg=0x801818AC;lower_bound=0x801818B4 0x801AF338 - - - - standalone grades=prologue 0x801AF704 - - - - standalone grades=prologue 0x801AF99C - - - - standalone grades=prologue diff --git a/tools/sf3_extents b/tools/sf3_extents index 4ed766e..74070a5 100755 --- a/tools/sf3_extents +++ b/tools/sf3_extents @@ -22,21 +22,28 @@ records the highest reachable instruction. The walk stops at: * a return (`jr ra`) -- the delay slot is included; * a register jump (`jr $rs`, rs != ra), i.e. a switch/jump table, whose targets cannot be followed statically; - * `syscall` / `break`; + * `break`, which traps; * a tail `j` whose target is outside this function (behind its start, or a known start); * another **hard** start, which is a boundary the walk may not cross. +`syscall` is deliberately **not** a terminator: on this target it is the BIOS +call instruction and it returns to the instruction after it. Treating it as a +return truncated the two 16-byte BIOS stubs at `0x80103FCC` and `0x80103FEC` to +8 bytes, which is unreachable from C because `cc1` always appends an epilogue +(found by worker C in Phase 8; the stub's four `jal` callers and the absence of +any reference to the following address are the confirming evidence). + The extent end is the first byte after the last reachable instruction. For a call this is the return address; for a return, the instruction after the delay slot. ## Grades - exact the walk terminated at a return / `syscall` / `break` / tail - jump and stayed inside its own region. This is the grade a match - candidate wants: the end is the function's own last byte + 1. - All 12 regions registered in Phase 6 reproduce at this grade. + exact the walk terminated at a return / `break` / tail jump and stayed + inside its own region. This is the grade a match candidate wants: + the end is the function's own last byte + 1. All 115 regions + registered in Phases 6-8 reproduce at this grade. fallthrough no terminal was reachable; the walk ran into the next hard start. The extent ends there. Typically a function that ends in a call to something that does not return. @@ -332,9 +339,12 @@ def walk(payload: bytes, text_address: int, start: int, hard: frozenset[int], pending.append(pc + 4) pc += 8 continue - if opcode == 0 and funct in (0x0C, 0x0D): # syscall / break - terminals.append(("syscall" if funct == 0x0C else "break", pc, None)) + if opcode == 0 and funct == 0x0D: # break (traps) + terminals.append(("break", pc, None)) break + if opcode == 0 and funct == 0x0C: # syscall (returns) + pc += 4 + continue if opcode == 0x02: # j target = jump_target(pc, word) # The delay slot executes and is part of the body, but control diff --git a/tools/tests/test_sf3_extents.py b/tools/tests/test_sf3_extents.py index 1114d4f..8be9bfa 100644 --- a/tools/tests/test_sf3_extents.py +++ b/tools/tests/test_sf3_extents.py @@ -114,7 +114,8 @@ def _payload() -> bytes: # No terminal: the body runs into the next hard start. put(FALLTHROUGH, _jal(PAYLOAD + PLAIN)) put(PLAIN, _jr()) - put(SYSCALL_FN, 0x0C) + put(SYSCALL_FN, 0x0C) # syscall (returns to the next word) + put(SYSCALL_FN + 4, _jr()) # ... so the body ends with a return # A branch whose target jumps over the next hard start. put(ESCAPE, _beq(6)) put(ESCAPE + 4, _jr()) @@ -253,10 +254,21 @@ class ExtentTests(unittest.TestCase): self.assertEqual(extent.grade, "fallthrough") self.assertEqual(extent.end, PAYLOAD + PLAIN) - def test_syscall_is_a_terminal(self) -> None: + def test_syscall_is_not_a_terminal_because_it_returns(self) -> None: + """A BIOS `syscall` returns to the next instruction, so the body continues.""" extent = self._extent(SYSCALL_FN) self.assertEqual(extent.grade, "exact") - self.assertIn("term=syscall", extent.evidence) + self.assertNotIn("term=syscall", extent.evidence) + # the walk ran past the syscall to the following `jr ra` and its delay slot + self.assertEqual(extent.end, PAYLOAD + SYSCALL_FN + 12) + self.assertIn("term=jr_ra", extent.evidence) + + def test_break_still_terminates(self) -> None: + payload = bytearray(_payload()) + struct.pack_into(" None: extent = self._extent(ESCAPE)