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.
This commit is contained in:
Christopher Williams
2026-09-23 23:11:38 -04:00
parent e98aa102e4
commit 62983c9a8a
3 changed files with 37 additions and 15 deletions
+5 -5
View File
@@ -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
1 # Syphon Filter 3 (USA) function extents.
2263 0x80103D60
2264 0x80103F84
2265 0x80103FB4
2266 0x80103FCC
2267 0x80103FDC
2268 0x80103FEC
2269 0x80103FFC
2270 0x8010400C
2271 0x80104040
2664 0x8016E504
2665 0x8016E60C
2666 0x8016E93C
2667 0x8017D5C0
2668 0x801800C4
2669 0x80180808
2670 0x8018080C
2671 0x801AF338
2672 0x801AF704
2673 0x801AF99C
+17 -7
View File
@@ -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
+15 -3
View File
@@ -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("<I", payload, SYSCALL_FN, 0x0D)
grades = {PAYLOAD + SYSCALL_FN: {"jal"}}
extent = sf3_extents.derive_extents(bytes(payload), PAYLOAD, grades)[0]
self.assertIn("term=break", extent.evidence)
def test_escape_is_clipped_and_records_the_crossing_instruction(self) -> None:
extent = self._extent(ESCAPE)