diff --git a/audit-runs/audit-059-handle-disambiguation/round-C1-setter-validation/FINDINGS.md b/audit-runs/audit-059-handle-disambiguation/round-C1-setter-validation/FINDINGS.md new file mode 100644 index 0000000..461280b --- /dev/null +++ b/audit-runs/audit-059-handle-disambiguation/round-C1-setter-validation/FINDINGS.md @@ -0,0 +1,127 @@ +# Phase C.1 — Validation refutes Phase A's bit-28 setter hypothesis + +## TL;DR + +Phase A claimed: "bit 28 of `[0x40d09a40]` (controller word) gets set in ours, causing sub_822F1AA8's dispatcher loop to exit early; candidate setter is `sub_821B55D8` at PC `0x821B5DA4`." + +**Phase C.1 falsifies this in 4 sub-rounds:** + +1. **`sub_821B55D8` is dead code** in both engines — its `XamInputSetState` wrapper `sub_824AA858` fires 0× in both. +2. **`[0x40d09a40]` is never set to anything with bit 28** — `--dump-addr` at end of run shows `+0x00 = 0x00000021`, the entry value. Bit 28 is NEVER set. +3. **The actual wedge is at the `bcctrl` at PC `0x822F1B4C`** (inside sub_822F1AA8 setup, BEFORE the dispatcher loop). tid=1 never reaches the loop top-check. +4. **The bcctrl calls `sub_82173990`** (vtable[0] of the dispatcher singleton at `[0x828E1F08]`), which eventually waits for tid=13 to terminate. tid=13 wedges in the audit-049 silph::UImpl@GamePart_Title chain on handle `0x1078`. + +The C.2 force-clear POC (the planned next step) would have **zero effect** because bit 28 is never set. Skipped per plan stopping criterion. + +## Probe-fire counts (ours, 50M-instr parallel) + +| PC | sub-round | fires | meaning | +|---|---|---|---| +| `0x821B55D8` (Phase A candidate fn entry) | 1 | **0** | function never reached → β/γ | +| `0x821B5D98,DA0,DAC,D48` (loop BB heads) | 1 | **0** | function never reached | +| `0x822F1AA8` (sub_822F1AA8 entry) | 2,3,4 | 2-3 | reached | +| `0x822F1B38` (post-`bl 0x824AA8B0`) | 4 | 2 | reached | +| `0x822F1B50` (post-`bcctrl`) | 4 | **0** | **bcctrl never returns** | +| `0x822F1B60,B78,B80,BBC` (loop setup/top) | 3 | 0 | unreachable past bcctrl | +| `0x822F1E10` (loop exit cleanup) | 2 | 0 | loop never entered, never exited | +| `0x822F1E34` (post-thread-join) | 2 | 0 | never reached | +| `0x82173990` (vtable[0] target) | 4 | 2 | called via bcctrl, r3=singleton (LR=0x822F1B50) | +| `0x821748F0` (tid=13 entry) | 4 | 2 | tid=13 runs | +| `0x821C4EB0` (silph::UImpl@GamePart_Title) | 4 | 2 | audit-009/049 reached on tid=13 | +| `0x82457388,0x824574C0,0x82457408,0x82457490` (other oris candidates) | 2 | 0 | unreachable | + +## Canary probe results + +| PC | fires | meaning | +|---|---|---| +| `0x824AA858` (XamInputSetState wrapper) | **0** | sub_821B55D8 chain is dead code in CANARY too | +| `0x822F1B50` (post-bcctrl, attempted) | **0** | canary's JitProlog only fires at function entries, so not directly testable; but per audit round-33 sub_821741C8 fires 471× in canary → bcctrl DOES return in canary | + +## Critical evidence: `--dump-addr=0x40d09a40` at end of run + +``` +addr=0x40d09a40 + +0x00: 00 00 00 21 00 00 00 01 42 44 df 00 40 54 1a 40 + ^^^^^^^^^^^ ^^^^^^^^^^^ + +0x10: 40 54 1b 40 40 54 1b 80 40 54 1b c0 00 00 10 54 + +0x20: 00 00 00 00 40 24 a8 20 00 00 00 08 00 00 00 00 +``` + +- `[+0x00] = 0x00000021` ← bit 28 (mask 0x10000000) is NOT SET. Same value as at sub_822F1AA8 entry. +- `[+0x1c] = 0x00001054` ← spawned init thread handle (= tid=8's thread handle, NOT 0x1070) +- Thread state: tid=1 waits on handle `0x1070`, tid=13 waits on handle `0x1078`. + +Handle `0x1070` is **tid=13's thread handle** (per stderr: `ExCreateThread: tid=13 handle=0x1070 entry=0x821748f0 ctx=0x4024a840 suspended=true`). So tid=1's wait at the wedge point is a **thread-join on tid=13**, NOT a thread-join on the dispatcher init thread (tid=8, handle 0x1054). + +## Wedge path (corrected) + +``` +entry_point (sub_824AB748) [tid=1 main] + └─ sub_8216EA68 + └─ sub_822F1AA8(controller=0x40d09a40) [LR=0x8216EE14] + ├─ ExCreateThread(entry=sub_822F1EE0, ctx=controller) [PC 0x822F1B08] + │ ⇒ tid=8 spawn, handle=0x1054 (suspended) + ├─ bl 0x824AA8B0 (no-op probe) [PC 0x822F1B34] + └─ bcctrl on vtable[+0] of [0x828E1F08] singleton [PC 0x822F1B4C] + │ + └─ sub_82173990(r3=singleton) [r3=0x40ba9a80, vtable=0x40111910] + └─ ... (768-byte function with ≥18 calls; calls sub_82448AA0, sub_824AA7A0, + sub_82448BC8, sub_82448C50, sub_8216F218, sub_8217C850, sub_82178E50, + sub_821835E0, ...) + └─ ... → KeWaitForSingleObject INFINITE on handle 0x1070 + (= tid=13's thread handle, thread-join) + ⇒ WEDGE — tid=13 never exits + +(Concurrently — spawned somewhere else, not from sub_822F1AA8:) +[tid=13, spawn-handle=0x1070, ctx=0x4024a840] + └─ sub_821748F0 (worker boilerplate, entry from ExCreateThread) + ├─ sub_82172798, sub_82172818 + └─ sub_821749C0 + └─ sub_821CF3F0 + └─ ... → sub_821C4EB0 (UImpl@GamePart_Title@silph) [audit-009/049!] + └─ ... → sub_821CB030 (creates KEVENT at +0x128) + ⇒ KeWaitForSingleObject INFINITE on handle 0x1078 + ⇒ WEDGE — handle 0x1078 is never signaled in ours +``` + +## Why Phase A's hypothesis is wrong + +Phase A: +1. Disassembled sub_822F1AA8's body, observed the bit-28 loop-exit check at `0x822F1BB8` and end-of-iter check at `0x822F1E0C`. +2. Mem-watch on `0x40d09a40` showed zero stores → inferred "the setter writes via some path mem-watch doesn't capture." +3. DB-scanned `oris ?, ?, 0x1000` (49 sites), found `sub_821B55D8 + 0x821B5DA4` with pattern `bl sub_824AA858 ; if r3 == 0xAA: oris r11, 0x1000 ; stw`. +4. Concluded `sub_821B55D8` was the setter. + +What Phase A missed: +- Mem-watch's 0-stores result was correct: **NO setter exists**. Bit 28 is never set in either engine. The mem-watch null-result was a hint that the bit-28 hypothesis itself was wrong, but Phase A interpreted it as "mem-watch misses something." +- The disasm-based hypothesis was visually compelling (a loop iterating arrays and setting bit 28 when a kernel call returns 0xAA) but never verified runtime. +- `sub_821B55D8` is itself dead code in both engines. + +## Reading-error class #19: disasm-pattern-match without runtime verification + +When scanning for a hypothesized signal source via DB pattern-match (`oris ?, ?, 0x1000`), the analyst must run a probe to verify the suspected site is *both reached* and *takes the suspected path* before declaring it the cause. Phase A bypassed both checks. The single `--dump-addr=0x40d09a40` flag in sub-round 2 (literally 4 keystrokes added to the existing probe command) revealed the central assumption was wrong. + +## Real divergence (handed to next session) + +This is the **same wedge as audit-049/058/059**: tid=13 wedges in the silph::UImpl@GamePart_Title cluster on handle `0x1078`. tid=1 wedges on tid=13's thread-handle (`0x1070`) inside `sub_82173990`'s call chain. + +`sub_82173990` is vtable[0] of the dispatcher singleton at `[0x828E1F08]`. It's a 768-byte function with ≥18 calls; the actual wait site is somewhere down its tree. To localize where in `sub_82173990` the wait happens, probe its BB heads + the `KeWaitForSingleObject` thunks (`sub_824AA330`, `sub_824AA708`). + +The fix-shape is **NOT** "force-clear bit 28." The fix-shape is **"signal handle 0x1078 in the audit-049 cluster, or short-circuit tid=13's wait."** Round 22 (silph_synth.rs) attempted the cluster-A version of this. Cluster B (silph::UImpl) needs its own synthesis or a kernel-side signal of handle 0x1078. + +## Phase C verdict + +- C.1: 4 sub-rounds executed (within budget). +- C.2: **NOT EXECUTED** — POC would be no-op since bit 28 is never set. Per plan stopping criterion, do not proceed to C.2 blind when C.1 refutes the diagnosis. +- C.3: not applicable. +- Branch state: no source changes. Audit artifacts only. + +## Files in this directory + +- `ours-c1-probe.log/stderr` — sub-round 1, probe at sub_821B55D8 BB heads (0 fires) +- `ours-sr2-confirm-bit28.log/stderr` — sub-round 2, probe loop top/exit + dump-addr (bit 28 NEVER SET) +- `ours-sr3-wait-trace.log/stderr` — sub-round 3, probe wait site + handle 0x1070 trace +- `ours-sr4-bcctrl-trace.log/stderr` — sub-round 4, probe pre/post bcctrl + sub_82173990 entry + tid=13 entry (decisive) +- canary side in `../round-C1-setter-validation-canary/`: + - `canary-824AA858.log` — XamInputSetState wrapper fires 0× in canary too + - `canary-822F1B50.log` — JitProlog can't probe at BB-internal PCs (function-entry-only)