vet_seeds: show all REJECTs in summary; handle non-nop delay slots

All rejected addresses now printed in the summary with their boundary
values, making false-reject investigation easy without re-running.

Boundary check now accepts jr $ra + any delay slot (not just nop), since
compilers often put a useful instruction in the delay slot. Non-nop delay
slots are noted in the verdict string for manual awareness.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-05-30 12:06:59 -07:00
parent 216cfd4211
commit cbfd499f2f
+22 -11
View File
@@ -95,24 +95,27 @@ def vet(exe_bytes, base_phys, text_size, vaddr):
if off < 8 or off + 16 > len(exe_bytes):
return 'REJECT', 'EXE offset out of range'
# Check 1: boundary
prev_jr = _word(exe_bytes, off - 8)
prev_nop = _word(exe_bytes, off - 4)
if not (_is_jr_ra(prev_jr) and _is_nop(prev_nop)):
# Check 1: boundary — jr $ra followed by any delay slot (nop is common
# but some compilers emit a useful instruction in the delay slot).
prev_jr = _word(exe_bytes, off - 8)
prev_ds = _word(exe_bytes, off - 4)
has_boundary = _is_jr_ra(prev_jr)
if not has_boundary:
return 'REJECT', (
f'no jr $ra/nop boundary '
f'(saw 0x{prev_jr:08X} / 0x{prev_nop:08X})'
f'(saw 0x{prev_jr:08X} / 0x{prev_ds:08X})'
)
# Note non-nop delay slot — still a valid boundary but worth flagging.
delay_note = '' if _is_nop(prev_ds) else f' (non-nop delay slot 0x{prev_ds:08X})'
# Check 2: prologue
for i in range(4):
if _is_addiu_sp_neg(_word(exe_bytes, off + i * 4)):
return 'ACCEPT', f'boundary + prologue at instr {i}'
return 'ACCEPT', f'boundary + prologue at instr {i}{delay_note}'
return 'WARN', (
'boundary found but no addiu $sp,$sp,-N in first 4 instrs '
'— frameless (leaf or frame-borrower); manual Ghidra check required '
'before seeding'
f'boundary + no addiu $sp,$sp,-N{delay_note}'
' -- frameless (leaf or frame-borrower); manual Ghidra check required'
)
# ---------------------------------------------------------------------------
@@ -178,8 +181,8 @@ def main():
accepted.append(vaddr)
elif verdict == 'WARN':
warned.append(vaddr)
else:
rejected.append(vaddr)
else: # REJECT
rejected.append((vaddr, reason))
print()
print(f'Results: {len(accepted)} ACCEPT {len(warned)} WARN {len(rejected)} REJECT')
@@ -196,5 +199,13 @@ def main():
for a in warned:
print(f' 0x{a:08X}')
if rejected:
print()
print('REJECT — mid-function or out-of-range (high confidence); rare false-rejects')
print(' possible when the preceding function ends with jr $t0 / jalr instead of jr $ra.')
print(' Investigate in Ghidra if you suspect a false-reject:')
for addr, reason in rejected:
print(f' 0x{addr:08X} # {reason}')
if __name__ == '__main__':
main()