phase11: tools/sf3_rank --fragments + cookbook 114 — worker A's fragment anomaly
Worker A found 0x800C3490 is not a matchable body: it starts mid-expression with sw v0,32(sp) before any frame setup, and its identical tail also appears at 0x800C3470, so it is a shared/jump-target block Ghidra promoted to a function -- inside no region, with the code before it in no worklist, so nobody can match it standalone. Its generalised rule is narrower than 'first instruction is not prologue-like', because a function may legally start with beq/sh/move: a row is a FRAGMENT if its first instruction touches the stack before any addiu sp,sp,-N, reads a stack slot, or uses a callee-saved register that is never saved. Implemented as a --fragments scan. Measured: 5 suspects across all four partitions, but 2 false positives across the 555 REGISTERED regions, so it is ADVISORY not an exclusion -- sufficient-but-not-complete like the trapping check. A hit means read before spending a spelling, never skip.
This commit is contained in:
@@ -1779,3 +1779,32 @@ it as taken. Worker A was wrongly blocked on exactly that address.
|
||||
range, not whether it appears as an endpoint.** Worker A wrote `.run/p11/w-a/free.sh <addr>` to do
|
||||
this correctly. **Any worker checking a candidate address against the registry must use a range
|
||||
test** — this is the third defect found in a coordinator-written rule this phase.
|
||||
|
||||
### 114. A FRAGMENT row can never match — worker A's anomaly scan (advisory)
|
||||
|
||||
Worker A found `0x800C3490` (132 B, in its partition, unclaimed) **is not a matchable body: it starts
|
||||
mid-expression.** Its first instruction is `sw v0,32(sp)` — a stack write before any frame setup,
|
||||
using `v0`/`v1`/`t1` set by the code *before* the row. It ends with its own
|
||||
`addiu sp,sp,48; jr ra`, and the identical tail also appears at `0x800C3470`–`0x800C348C`, so it is a
|
||||
**shared/jump-target block that Ghidra promoted to a function**. It is inside no region and the code
|
||||
before it is in no worklist, so **nobody can match it standalone**.
|
||||
|
||||
**Worker A's generalised rule, and the reason it is narrower than "first instruction is not
|
||||
prologue-like":** a function may legally start with `beq a0,a1,...` (comparing two incoming
|
||||
registers) or `sh` (storing an incoming register to `*a0`), so most such rows are fine. The real
|
||||
signal is:
|
||||
|
||||
> **A row is a FRAGMENT if its first instruction writes to the stack (sp-relative) before any
|
||||
> `addiu sp,sp,-N`, or reads a stack slot, or uses a callee-saved register that is never saved.**
|
||||
|
||||
Worker A scanned its 256 rows and got 11 first-instruction hits, of which **only `0x800C3490`**
|
||||
trips the real rule.
|
||||
|
||||
**Implemented as `./tools/sf3_rank --fragments`, which scans any partition set.** Measured across
|
||||
all four partitions it finds **5 suspects**; measured against the **555 registered regions it flags
|
||||
2** (`0x800923E8`, `0x80099DC4`), so it is **advisory, not an exclusion** — sufficient-but-not-complete
|
||||
in exactly the way the trapping check is. **A hit means "read this before spending a spelling", never
|
||||
"skip it".**
|
||||
|
||||
**The value is the reading phase, not the match:** a fragment costs a full structural derivation and
|
||||
can never close, so flagging it saves a worker's whole reading budget.
|
||||
|
||||
+63
-2
@@ -77,6 +77,45 @@ def redundancy(words: list[int]) -> float:
|
||||
return max(scores) if scores else 0.0
|
||||
|
||||
|
||||
CALLEE_SAVED = frozenset(range(16, 24)) # s0..s7
|
||||
|
||||
|
||||
def fragment_reason(insns: list[int]) -> str:
|
||||
"""Worker A's fragment check: does the body use a frame it never set up?
|
||||
|
||||
A function may legally start with `beq a0,a1,...` or `sh a0,0(a0)`. It may NOT start by
|
||||
touching a slot of a frame that does not exist yet. Worker A found `0x800C3490`, whose
|
||||
first instruction is `sw v0,32(sp)` -- a stack write before any frame setup, using
|
||||
registers set by the code BEFORE the row -- and whose identical tail also appears at
|
||||
0x800C3470. It is a shared/jump-target block Ghidra promoted to a function, so it is
|
||||
inside no region and nobody can match it standalone.
|
||||
|
||||
ADVISORY, NOT AN EXCLUSION: measured against the 555 registered regions this flags
|
||||
**2** of them (`0x800923E8`, `0x80099DC4`, both "first insn touches sp+16"), so it is
|
||||
sufficient-but-not-complete in exactly the way the trapping check is. Treat a hit as
|
||||
"read this before spending a spelling", never as "skip".
|
||||
"""
|
||||
if not insns:
|
||||
return "empty"
|
||||
word = insns[0]
|
||||
opcode = word >> 26
|
||||
rs = (word >> 21) & 0x1F
|
||||
rt = (word >> 16) & 0x1F
|
||||
immediate = word & 0xFFFF
|
||||
if opcode in (0x23, 0x2B, 0x0F, 0x20, 0x24, 0x25, 0x28, 0x29, 0x2C, 0x2D, 0x2E, 0x3F):
|
||||
if (rs == 29 or rt == 29) and immediate != 0:
|
||||
return f"first instruction touches sp+{immediate}"
|
||||
used = {rs, rt}
|
||||
if opcode == 0:
|
||||
used.add((word >> 11) & 0x1F)
|
||||
hit = used & CALLEE_SAVED
|
||||
if hit and opcode != 0x09 and len(insns) > 8:
|
||||
saves = any((x >> 26) == 0x2B and ((x >> 21) & 0x1F) == 29 for x in insns[:12])
|
||||
if not saves:
|
||||
return f"uses {sorted(hit)} with no save in the first 12 instructions"
|
||||
return ""
|
||||
|
||||
|
||||
def load_rows(paths: list[Path], payload: bytes, min_size: int, max_size: int) -> list[tuple[float, int, str]]:
|
||||
rows: list[tuple[float, int, str]] = []
|
||||
for path in paths:
|
||||
@@ -93,11 +132,13 @@ def load_rows(paths: list[Path], payload: bytes, min_size: int, max_size: int) -
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
paths, top, min_size, max_size = [], 40, 0, 1 << 30
|
||||
paths, top, min_size, max_size, fragments = [], 40, 0, 1 << 30, False
|
||||
index = 0
|
||||
while index < len(argv):
|
||||
arg = argv[index]
|
||||
if arg == "--top":
|
||||
if arg == "--fragments":
|
||||
fragments = True
|
||||
elif arg == "--top":
|
||||
index += 1
|
||||
top = int(argv[index])
|
||||
elif arg == "--min-size":
|
||||
@@ -120,6 +161,26 @@ def main(argv: list[str]) -> int:
|
||||
return 2
|
||||
|
||||
payload = EXE.read_bytes()
|
||||
|
||||
if fragments:
|
||||
print("ADVISORY ONLY -- measured at 2 false positives across 555 registered regions.")
|
||||
print(f"{'size':>5} address reason")
|
||||
count = 0
|
||||
for path in paths:
|
||||
for line in path.read_text().splitlines():
|
||||
if line.startswith("#") or not line.strip():
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
start, end = int(fields[1], 16), int(fields[2], 16)
|
||||
if end - start < min_size or end - start > max_size:
|
||||
continue
|
||||
reason = fragment_reason(body_words(payload, start, end))
|
||||
if reason:
|
||||
count += 1
|
||||
print(f"{end - start:5d} {fields[1]} {reason}")
|
||||
print(f"\nsuspects={count}")
|
||||
return 0
|
||||
|
||||
rows = load_rows(paths, payload, min_size, max_size)
|
||||
rows.sort(key=lambda row: (-row[0], row[1], row[2]))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user