From 8683fb59ed07115c208336af0416b3b7c279658b Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 21:04:04 +0200 Subject: [PATCH] [2.BF] Silph WorkerCtx: synthesize sub-object + vtable at [+0x2C] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-059 round 19 isolated the round-18 worker fault: the four silph:: WorkerCtx worker bodies all execute the sequence lwz r3, 44(rN) ; r3 = [ctx+0x2C] — sub-object pointer lwz r11, 0(r3) ; r11 = sub-object vtable lwz r11, 60(r11) ; r11 = sub-object vtable[15] mtctr r11 bctrl Ours left [ctx+0x2C] NULL → PC=0 fault on first virtual dispatch. Round 19 recommended materialising a sub-object whose vtable points entirely at an existing trivial-return stub so workers idle live, returning NULL work, without crashing. Changes (silph_synth.rs only, +63/-6): - Grow SILPH_CTX_SIZE 0x500 → 0x800 to embed sub-object at +0x300 and a 32-slot sub-vtable at +0x500 in the same heap_alloc. - After ctx header init, write sub-object pointer at [ctx+0x2C], the XEX- resident wrapper constant 0xBE568F00 (round-7 finding) at [ctx+0x30], and leave [ctx+0x28] NULL (matches canary first-fire snapshot). - Populate every slot of the 32-entry sub-vtable with VA 0x8216CAA4, the first 4-byte-aligned standalone `li r3, 0; blr` stub located by a fresh PE-text scan (preceded by a `blr` terminating the previous function). - Sub-object body itself is zero-filled apart from the [+0]=vtable_ptr write; round-19 disassembly confirms workers only touch slots 15/17. Smoke (XENIA_SILPH_SYNTH=1, persistent cache, 5e7 instr): - Lockstep: no crash, all 4 workers (tid=6/7/8/9) reach Ready in deep worker-body PCs (0x825067xx/0x825089xx/0x825091xx). Verdict (D) — workers run their idle loop returning NULL; existing silph waiters (0x1020, 0x1090) remain because we deliberately neutered productive work. - Parallel: identical picture, no PC=0/PC=garbage fault anywhere. No regression in 765-test suite. Next round: feed real work-items into the intrusive ring at ctx+0x210 so workers' returned-NULL idle becomes returned-work productive; or discover which sub-vtable slots actually need real callees (slot 15 worker drain, slot 17 producer). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/xenia-kernel/src/silph_synth.rs | 69 +++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/crates/xenia-kernel/src/silph_synth.rs b/crates/xenia-kernel/src/silph_synth.rs index f32d6ca..14c21a0 100644 --- a/crates/xenia-kernel/src/silph_synth.rs +++ b/crates/xenia-kernel/src/silph_synth.rs @@ -53,9 +53,40 @@ const SILPH_WORKER_ENTRIES: [u32; 4] = [ 0x8250_65B8, ]; -/// Round 0x440 up to a page so the ctx alloc never straddles a page boundary -/// in heap_alloc's bookkeeping. -const SILPH_CTX_SIZE: u32 = 0x500; +/// 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 +/// 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 +/// the worker bodies dereference `[ctx+0x2C]` to obtain a sub-object pointer +/// and then virtual-dispatch through its slot 15. +const SILPH_CTX_SIZE: u32 = 0x800; + +/// Offset within the ctx allocation of the synthetic sub-object referenced +/// at `[ctx+0x2C]`. Canary's sub-object sits ~0x300 bytes above the ctx and +/// varies per-instance; we keep it embedded in the same alloc so a single +/// `heap_alloc` covers everything. +const SILPH_SUBOBJ_OFFSET: u32 = 0x300; + +/// Offset within the ctx allocation of the synthetic sub-object vtable. +const SILPH_SUBVTBL_OFFSET: u32 = 0x500; + +/// Number of slots in the synthetic sub-vtable. The worker bodies access +/// slots 15 and 17 (per audit-059 round 19); 32 slots is generous and gives +/// headroom for any downstream vtable index we haven't surveyed yet. +const SILPH_SUBVTBL_SLOTS: u32 = 32; + +/// Trivial `li r3, 0; blr` stub at VA `0x8216CAA4` in the XEX `.text` +/// section — the first 4-byte-aligned standalone instance located by the +/// round-20 PE scan (preceded by a `blr` ending the previous function). +/// We populate every slot of the synthetic sub-vtable with this address so +/// any virtual call through the sub-object idles, returning NULL work and +/// keeping the worker live. +const SILPH_STUB_RETURN_NULL_VA: u32 = 0x8216_CAA4; + +/// 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 +/// pointer / handle the worker passes through without dereferencing. +const SILPH_CTX_FIELD_30_CONST: u32 = 0xBE56_8F00; /// 64 KiB worker stack (mirrors AUDIT-048 audio worker), half of canary's /// 128 KiB default. @@ -109,9 +140,35 @@ pub fn spawn_silph_workers( mem.write_u32(ctx + 0x18, 0x3F7F_CCCC); // float ~1.0 (UI rate A) mem.write_u32(ctx + 0x1C, 0x3F80_2D83); // float ~1.0 (UI rate B) mem.write_u32(ctx + 0x24, 0x0000_0001); - // +0x28..+0x30 = three foreign pointers (heap arenas BE/701C/BCA4/B1B6 - // per audit-059 round 7). Left NULL — if any worker dereferences these - // we'll see a guest fault and treat that as the next gate. + + // +0x28..+0x30 = three foreign pointers. + // +0x28 — canary's first-fire snapshot has NULL here. Round-19 fault + // analysis shows worker bodies don't dereference this on + // first entry, so we leave it NULL too. + // +0x2C — sub-object pointer. Worker bodies do + // `lwz r3,44(rN); lwz r11,0(r3); lwz r11,60(r11); bctrl`, + // i.e. virtual-dispatch through slot 15 of the sub-object's + // vtable. Point this at our synthesised sub-object embedded + // at ctx + SILPH_SUBOBJ_OFFSET. + // +0x30 — XEX-resident wrapper constant 0xBE568F00 (round 7). Opaque + // but identical across all four canary ctxes. + 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 + 0x30, SILPH_CTX_FIELD_30_CONST); + + // ---- Synthetic sub-object at +0x300 ---- + // Round-19 disassembly shows worker bodies only touch the sub-object's + // vtable (slots 15/17) and treat the rest as opaque. Zero-fill is + // sufficient. The only required field is `[+0]` = sub-vtable pointer. + mem.write_u32(subobj_ptr, subvtbl_ptr); + + // ---- Synthetic sub-vtable at +0x500 ---- + // Every slot points at SILPH_STUB_RETURN_NULL_VA so any virtual call + // through the sub-object idles, returning NULL work without crashing. + for slot in 0..SILPH_SUBVTBL_SLOTS { + mem.write_u32(subvtbl_ptr + slot * 4, SILPH_STUB_RETURN_NULL_VA); + } // ---- 4× X_KEVENT auto-reset at +0x54/+0x64/+0x74/+0x84, state = 0 ---- // X_DISPATCH_HEADER layout (canary xobject.h:35):