[2.BF] Silph WorkerCtx: install canary's real sub-vtable at [+0x2C][0]

Round-21 pivot of the audit-059 synth-spawn module. Round 20 made the
silph::WorkerCtx workers run by attaching a 32-slot stub sub-vtable
where every entry was a `li r3, 0; blr` stub — workers spawned but
spun forever because slots 15/17 short-circuited to NULL ("no work").

Round 21 reads canary's real sub-vtable VA out of the XEX `.rdata` —
`0x8200A168` — and points `[sub_object + 0]` at it directly. The
vtable bytes live in the static image both engines map, so no guest
memory is consumed and slot 15 (= `sub_824FCCC8`) and slot 17
(= `sub_824FCE38`) — the only slots `sub_82506B08` ever calls —
become working game methods.

Discovery method (canary probes in
`audit-runs/audit-059-handle-disambiguation/round21-subvtable-canary/`):
  1. `--audit_jit_prolog_pc=0x82506B08` to catch the first WorkerCtx
     virtual-dispatch entry; `[r3+0x2C]` revealed the sub-object VA.
  2. Re-run with `--audit_jit_prolog_mem_dump=<sub-obj VA>` to deref
     `[sub-object + 0]` = sub-vtable VA = 0x8200A168.
  3. PE inspection (`xex-text/xex-rdata` is the static image) reads
     all 31 slots; slot 15 -> sub_824FCCC8, slot 17 -> sub_824FCE38.

Smoke metrics (50M instructions, `XENIA_CACHE_PERSIST=1
XENIA_SILPH_SYNTH=1`, audit-runs/audit-059-handle-disambiguation/
round21-real-vtable/):
  * 4/4 workers spawned, no crash, no new fault
  * KeSetEvent 633885 -> 431860 (-32%)
  * KeWaitForSingleObject 258441 -> 185762 (-28%)
  * Per-handle state unchanged on the focused stalled set
    (0x1020/0x1090 still `<NO_SIGNALS_DESPITE_WAITS>`,
    0x12a4/0x12ac/0x1218/0x1224 still `<UNCREATED>`).
  * No VdSwap/draws progression observed in this window.

Verdict: B (partial). The workers no longer spin in a stub-loop —
internal call density shifted — but the focused wedge handles still
don't get signalled. Likely root cause: workers may now be waiting
on the WorkerCtx's own KEVENTs (which we synthesised at
+0x54/+0x94) for upstream work that no producer is enqueuing.

Net LOC: 29 ins / 31 del. Tests: workspace passes (lockstep app
tests, kernel 127/127, hir 288/288, scheduler 38/38).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 21:19:52 +02:00
parent 8683fb59ed
commit 40f208ea4e

View File

@@ -56,9 +56,10 @@ const SILPH_WORKER_ENTRIES: [u32; 4] = [
/// Round 0x440 up to a page-ish so the ctx alloc never straddles a page /// Round 0x440 up to a page-ish so the ctx alloc never straddles a page
/// boundary in heap_alloc's bookkeeping. Round 20 grew the alloc from 0x500 /// boundary in heap_alloc's bookkeeping. Round 20 grew the alloc from 0x500
/// to 0x800 to make room for a synthesised sub-object at +0x300 and its /// to 0x800 to make room for a synthesised sub-object at +0x300 and its
/// 32-slot vtable at +0x500 (= ctx + 0x500..0x580). Both are required because /// 32-slot vtable at +0x500 (= ctx + 0x500..0x580). Round 21 retains the
/// the worker bodies dereference `[ctx+0x2C]` to obtain a sub-object pointer /// embedded sub-object but drops the synthesized vtable (we now point at
/// and then virtual-dispatch through its slot 15. /// canary's real XEX-resident sub-vtable directly), so the 0x500..0x580
/// region is unused but harmless.
const SILPH_CTX_SIZE: u32 = 0x800; const SILPH_CTX_SIZE: u32 = 0x800;
/// Offset within the ctx allocation of the synthetic sub-object referenced /// Offset within the ctx allocation of the synthetic sub-object referenced
@@ -67,21 +68,20 @@ const SILPH_CTX_SIZE: u32 = 0x800;
/// `heap_alloc` covers everything. /// `heap_alloc` covers everything.
const SILPH_SUBOBJ_OFFSET: u32 = 0x300; const SILPH_SUBOBJ_OFFSET: u32 = 0x300;
/// Offset within the ctx allocation of the synthetic sub-object vtable. /// XEX `.rdata` VA of canary's real sub-object vtable (audit-059 round 21).
const SILPH_SUBVTBL_OFFSET: u32 = 0x500; /// Discovered by:
/// 1. Probing canary at `pc=0x82506B08` (= `sub_82506B08`, method 35 of
/// Number of slots in the synthetic sub-vtable. The worker bodies access /// the WorkerCtx vtable, the first sub-object method called by every
/// slots 15 and 17 (per audit-059 round 19); 32 slots is generous and gives /// `sub_82506528/58/88/B8` worker entry).
/// headroom for any downstream vtable index we haven't surveyed yet. /// 2. Capturing `[ctx+0x2C]` from the JIT-prolog dump (= sub-object VA
const SILPH_SUBVTBL_SLOTS: u32 = 32; /// in canary's heap).
/// 3. Re-running with `--audit_jit_prolog_mem_dump=<sub-obj VA>` to read
/// Trivial `li r3, 0; blr` stub at VA `0x8216CAA4` in the XEX `.text` /// `[sub-object + 0]` = sub-vtable VA = **`0x8200A168`**.
/// section — the first 4-byte-aligned standalone instance located by the /// PE inspection confirms slot 15 (called via `[r11+0x3C]` at
/// round-20 PE scan (preceded by a `blr` ending the previous function). /// `sub_82506B08+0x44`) = `sub_824FCCC8` and slot 17 (`[r11+0x44]` at
/// We populate every slot of the synthetic sub-vtable with this address so /// `sub_82506B08+0x70`) = `sub_824FCE38`. Both are real game methods in
/// any virtual call through the sub-object idles, returning NULL work and /// the same `.text` region as the rest of the worker dispatch surface.
/// keeping the worker live. const SILPH_SUB_VTABLE_SOURCE_VA: u32 = 0x8200_A168;
const SILPH_STUB_RETURN_NULL_VA: u32 = 0x8216_CAA4;
/// Round-19 XEX-resident wrapper constant observed at `[ctx+0x30]` in every /// Round-19 XEX-resident wrapper constant observed at `[ctx+0x30]` in every
/// canary ctx (audit-059 round 7). Same value for all four ctxes — opaque /// canary ctx (audit-059 round 7). Same value for all four ctxes — opaque
@@ -153,22 +153,20 @@ pub fn spawn_silph_workers(
// +0x30 — XEX-resident wrapper constant 0xBE568F00 (round 7). Opaque // +0x30 — XEX-resident wrapper constant 0xBE568F00 (round 7). Opaque
// but identical across all four canary ctxes. // but identical across all four canary ctxes.
let subobj_ptr = ctx + SILPH_SUBOBJ_OFFSET; let subobj_ptr = ctx + SILPH_SUBOBJ_OFFSET;
let subvtbl_ptr = ctx + SILPH_SUBVTBL_OFFSET;
mem.write_u32(ctx + 0x2C, subobj_ptr); mem.write_u32(ctx + 0x2C, subobj_ptr);
mem.write_u32(ctx + 0x30, SILPH_CTX_FIELD_30_CONST); mem.write_u32(ctx + 0x30, SILPH_CTX_FIELD_30_CONST);
// ---- Synthetic sub-object at +0x300 ---- // ---- Embedded sub-object at +0x300 ----
// Round-19 disassembly shows worker bodies only touch the sub-object's // Round-21 pivot: instead of synthesising a stub vtable that returns
// vtable (slots 15/17) and treat the rest as opaque. Zero-fill is // NULL from every slot, point `[sub_object + 0]` directly at canary's
// sufficient. The only required field is `[+0]` = sub-vtable pointer. // real XEX-resident sub-vtable VA. The vtable bytes are part of the
mem.write_u32(subobj_ptr, subvtbl_ptr); // same static image both engines map, so referring to it costs zero
// guest memory and gives the workers a working virtual-method surface
// ---- Synthetic sub-vtable at +0x500 ---- // (slot 15 = sub_824FCCC8, slot 17 = sub_824FCE38, plus 29 other real
// Every slot points at SILPH_STUB_RETURN_NULL_VA so any virtual call // methods). Round-19 disassembly shows worker bodies only touch the
// through the sub-object idles, returning NULL work without crashing. // sub-object's vtable; the rest of the sub-object is opaque so we
for slot in 0..SILPH_SUBVTBL_SLOTS { // leave it zero-filled.
mem.write_u32(subvtbl_ptr + slot * 4, SILPH_STUB_RETURN_NULL_VA); mem.write_u32(subobj_ptr, SILPH_SUB_VTABLE_SOURCE_VA);
}
// ---- 4× X_KEVENT auto-reset at +0x54/+0x64/+0x74/+0x84, state = 0 ---- // ---- 4× X_KEVENT auto-reset at +0x54/+0x64/+0x74/+0x84, state = 0 ----
// X_DISPATCH_HEADER layout (canary xobject.h:35): // X_DISPATCH_HEADER layout (canary xobject.h:35):