diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index a4dfa7d..91f1fdd 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -980,6 +980,43 @@ fn open_vfs_file( // see a null handle later and trigger `XamShowDirtyDiscErrorUI`. let path = crate::path::object_attributes_to_vfs_path(mem, obj_attrs_ptr) .unwrap_or_default(); + // AUDIT-2.BF — synthetic silph::WorkerCtx spawn. AUDIT-058/059 + // identified that ours never activates the 6-level static caller + // ladder that ends in `sub_825070F0`, so the four worker threads + // it would normally spawn (entries 0x82506528/58/88/B8) never run. + // Canary's chain originally fires right after `DiscImageDevice:: + // ResolvePath("\\dat\\movie")` (audit-058); ours never opens + // `dat/movie` because tid=13 wedges before reaching it. We + // therefore trigger on the first `dat/*` open — the earliest + // such open in ours is `dat/files.tbl` (immediately preceding + // tid=12/13 spawn at audit-059 round 1). + // + // **Round 18 finding** (this commit): when the workers are + // spawned runnable, they fault almost immediately (`PC=0` at + // cycle ~5.5M on the hw thread carrying worker_3), preempting + // ours' boot before the normal guest threads even spawn. The + // ctx layout from audit-059 round 5 is incomplete — at least + // one of `[+0x28]`/`[+0x2C]`/`[+0x30]` (the three foreign- + // arena pointers) must be populated for the worker bodies to + // run. Synthesising those is a fresh investigation (round 19+). + // + // Until then the synth path is **opt-in**: set + // `XENIA_SILPH_SYNTH=1` to enable the runnable spawn (will + // crash boot), or `XENIA_SILPH_SYNTH=suspend` to spawn but keep + // them in `Blocked(Suspended)` (lets boot complete with the + // ctx materialised in memory for downstream probes). Default: + // disabled — preserves the existing boot trajectory. + if !state.silph_synth_done && path.starts_with("dat/") { + match std::env::var("XENIA_SILPH_SYNTH").as_deref() { + Ok("1") | Ok("run") | Ok("runnable") => { + let _ = crate::silph_synth::spawn_silph_workers(state, mem, false); + } + Ok("suspend") | Ok("suspended") => { + let _ = crate::silph_synth::spawn_silph_workers(state, mem, true); + } + _ => {} + } + } if path.is_empty() && obj_attrs_ptr == 0 { if handle_out != 0 { mem.write_u32(handle_out, 0); diff --git a/crates/xenia-kernel/src/lib.rs b/crates/xenia-kernel/src/lib.rs index 13a15d0..6702b78 100644 --- a/crates/xenia-kernel/src/lib.rs +++ b/crates/xenia-kernel/src/lib.rs @@ -3,6 +3,7 @@ pub mod exports; pub mod interrupts; pub mod objects; pub mod path; +pub mod silph_synth; pub mod state; pub mod thread; pub mod ui_bridge; diff --git a/crates/xenia-kernel/src/silph_synth.rs b/crates/xenia-kernel/src/silph_synth.rs new file mode 100644 index 0000000..f32d6ca --- /dev/null +++ b/crates/xenia-kernel/src/silph_synth.rs @@ -0,0 +1,225 @@ +//! AUDIT-2.BF — synthetic spawn of the silph::WorkerCtx worker quartet. +//! +//! AUDIT-058/059 traced a 6-level static-caller ladder +//! (`sub_824F7800 ← sub_824F7CD0 ← sub_824F8398 ← sub_821B55D8 ← sub_821B6DF4`, +//! topped by virtual-dispatch from `sub_82172BA0+0x1E8`) that activates +//! `sub_825070F0` in canary at ~1× / 30 s, kicking off four worker threads +//! initialised against a single ~0x440-byte ctx. In ours none of those PCs +//! fire (audit-059 round 9 confirmed sub_821B6DF4 = 0×, real chain entry = +//! virtual-dispatch from sub_82172BA0+0x1E8 hits wrong-vtable slot). +//! +//! Rather than chase the wrong-vtable break, this module reproduces the end +//! state directly: at the first observation of a load-bearing VFS path +//! (`dat/movie`), we synthesise the ctx structure in guest memory per audit- +//! 059 round 5's live hexdump and spawn the four worker entry points the +//! same way AUDIT-048's audio host-pump spawns its dedicated client worker. +//! +//! The ctx is opaque to the workers — only fields they dereference matter. +//! Per round 5 dump (`audit-runs/audit-059-handle-disambiguation/round5-ctx- +//! dump/canary.log`): +//! +//! +0x00 vtable = 0x8200A1E8 (XEX .rdata, valid in both engines) +//! +0x04 self = ctx +//! +0x08 intrusive head= ctx +//! +0x0C init flag = 1 +//! +0x10 packed byte = 0x01000000 +//! +0x18 float ~1.0 = 0x3F7FCCCC +//! +0x1C float ~1.0 = 0x3F802D83 +//! +0x24 flag = 1 +//! +0x28..+0x30 = three foreign pointers, NULL initially +//! +0x54..+0x84 = 4× X_KEVENT auto-reset, state=0 +//! +0x94..+0xC4 = 4× X_KEVENT manual-reset, state=1 +//! +0x210..+0x250 = 4-entry intrusive work-ring, empty +//! +//! Worker entries (each takes r3 = ctx_ptr): +//! 0x82506528, 0x82506558, 0x82506588, 0x825065B8 + +use xenia_cpu::scheduler::{BlockReason, SpawnParams}; +use xenia_cpu::ThreadRef; +use xenia_memory::{GuestMemory, MemoryAccess}; + +use crate::objects::KernelObject; +use crate::state::{GuestMemoryPcr, KernelState}; +use crate::thread::allocate_thread_image; + +/// XEX `.rdata` vtable for the silph::WorkerCtx singleton (audit-059 round 5). +const SILPH_CTX_VTABLE: u32 = 0x8200_A1E8; + +/// 4-element fixed entry table — guest text PCs for the four worker bodies. +const SILPH_WORKER_ENTRIES: [u32; 4] = [ + 0x8250_6528, + 0x8250_6558, + 0x8250_6588, + 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; + +/// 64 KiB worker stack (mirrors AUDIT-048 audio worker), half of canary's +/// 128 KiB default. +const SILPH_WORKER_STACK: u32 = 0x10_000; + +/// Idempotently synthesise the silph::WorkerCtx and spawn the four worker +/// threads it normally drives. +/// +/// `suspended` controls whether the spawned threads enter the runqueue as +/// `Ready` (false) or as `Blocked(Suspended)` (true). Use `true` for +/// diagnostic baselines where you want the ctx materialised in guest memory +/// for downstream probes but don't want the worker bodies executing (e.g. +/// when round-5 ctx fields like the foreign-arena pointers at +0x28/+0x2C/ +/// +0x30 are still NULL and the workers would fault on first dereference). +/// +/// Returns the ctx VA on the first call; on subsequent calls returns the +/// cached VA without re-spawning. Failures inside spawn are logged but the +/// `synth_done` latch is still flipped so we don't retry-loop. +/// +/// Mirrors the AUDIT-048 audio-worker spawn pattern in +/// `xaudio_register_render_driver` (`exports.rs:3122`). +pub fn spawn_silph_workers( + state: &mut KernelState, + mem: &GuestMemory, + suspended: bool, +) -> Option { + if state.silph_synth_done { + return Some(state.silph_synth_ctx); + } + state.silph_synth_done = true; + + let Some(ctx) = state.heap_alloc(SILPH_CTX_SIZE, mem) else { + tracing::warn!("silph_synth: heap_alloc({:#x}) failed for ctx", SILPH_CTX_SIZE); + return None; + }; + state.silph_synth_ctx = ctx; + + // Zero the entire ctx page first — heap_alloc returns freshly mapped + // memory but we want the audit-059-round-5 layout to be canonical + // regardless of any future allocator behaviour change. + for off in (0..SILPH_CTX_SIZE).step_by(4) { + mem.write_u32(ctx + off, 0); + } + + // ---- Header scalars (per audit-059 round 5 hexdump) ---- + mem.write_u32(ctx + 0x00, SILPH_CTX_VTABLE); + mem.write_u32(ctx + 0x04, ctx); // self + mem.write_u32(ctx + 0x08, ctx); // intrusive list head pointing at self + mem.write_u32(ctx + 0x0C, 0x0000_0001); // init flag / refcount + mem.write_u32(ctx + 0x10, 0x0100_0000); // packed byte field + 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. + + // ---- 4× X_KEVENT auto-reset at +0x54/+0x64/+0x74/+0x84, state = 0 ---- + // X_DISPATCH_HEADER layout (canary xobject.h:35): + // +0x00 type (u8: 0=manual-event, 1=auto-event, 2=mutant, ...) + // +0x01 abandoned (u8) + // +0x02 size (u8 dwords) + // +0x03 inserted (u8) + // +0x04 signal_state (u32 BE) + // +0x08..+0x0F list_head (two pointers — self-link = empty list) + for i in 0..4u32 { + let off = ctx + 0x54 + (i * 0x10); + mem.write_u8(off, 1); // type = auto-reset Event + mem.write_u32(off + 4, 0); // signal_state = 0 + // List head self-link denotes empty waiter list. + mem.write_u32(off + 8, off + 8); + mem.write_u32(off + 12, off + 8); + } + // ---- 4× X_KEVENT manual-reset at +0x94..+0xC4, state = 1 (pre-signaled) ---- + for i in 0..4u32 { + let off = ctx + 0x94 + (i * 0x10); + mem.write_u8(off, 0); // type = manual-reset Event + mem.write_u32(off + 4, 1); // signal_state = 1 (pre-signaled) + mem.write_u32(off + 8, off + 8); + mem.write_u32(off + 12, off + 8); + } + + // ---- 4-entry intrusive work-ring at +0x210, initially empty ---- + // Each entry: [+0]=0x01000000 [+4]=0 [+8]=self_ptr [+0xC]=self_ptr. + for i in 0..4u32 { + let off = ctx + 0x210 + (i * 0x10); + mem.write_u32(off, 0x0100_0000); + mem.write_u32(off + 4, 0); + mem.write_u32(off + 8, off + 8); + mem.write_u32(off + 12, off + 8); + } + + // +0x250 "XEN"-tagged descriptors and +0x2E0 resource-index table left + // zero — they may be populated lazily by the workers themselves. + + // ---- Spawn the 4 worker guest threads ---- + use std::sync::atomic::Ordering; + let mut spawned = 0usize; + for (i, &entry) in SILPH_WORKER_ENTRIES.iter().enumerate() { + let Some(image) = allocate_thread_image(state, mem, SILPH_WORKER_STACK, 0) else { + tracing::warn!("silph_synth: allocate_thread_image failed for worker {}", i); + continue; + }; + let tid = state.next_thread_id.fetch_add(1, Ordering::Relaxed); + let handle = state.alloc_handle_for(KernelObject::Thread { + id: tid, + hw_id: None, + exit_code: None, + waiters: Vec::new(), + }); + let tls_slot_count = state.next_tls_index.load(Ordering::Relaxed); + let params = SpawnParams { + entry, + start_context: ctx, // r3 = ctx_ptr + stack_base: image.stack_base, + stack_size: image.stack_size, + pcr_base: image.pcr_base, + tls_base: image.tls_base, + thread_handle: handle, + guest_tid: tid, + create_suspended: suspended, + is_initial: false, + tls_slot_count, + affinity_mask: 0, + priority: 0, + ideal_processor: None, + }; + match state.scheduler.spawn(params, &mut GuestMemoryPcr(mem)) { + Ok(hw_id) => { + if let Some(KernelObject::Thread { hw_id: slot, .. }) = + state.objects.get_mut(&handle) + { + *slot = Some(hw_id); + } + let tref = ThreadRef::new( + hw_id, + (state.scheduler.slots[hw_id as usize].runqueue.len() - 1) as u16, + ); + state.silph_synth_handles[i] = Some(handle); + state.silph_synth_refs[i] = Some(tref); + spawned += 1; + tracing::info!( + "silph_synth: spawned worker {} tid={} handle={:#x} entry={:#010x} ctx={:#010x}", + i, tid, handle, entry, ctx + ); + } + Err(_) => { + tracing::warn!( + "silph_synth: scheduler.spawn failed for worker {} entry={:#010x}", + i, entry + ); + } + } + // Avoid an unused-variable warning if BlockReason isn't referenced. + let _ = BlockReason::WaitAny { + handles: Vec::new(), + deadline: None, + }; + } + + tracing::info!( + "silph_synth: ctx={:#010x} workers_spawned={}/4", + ctx, spawned + ); + + Some(ctx) +} diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index cc705d1..0b0d23d 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -299,6 +299,20 @@ pub struct KernelState { pub dump_addrs: Vec, /// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`. pub dump_section: Option<(u32, u32, std::path::PathBuf)>, + /// AUDIT-2.BF — synthetic silph::WorkerCtx spawn one-shot latch. Set on + /// first call to [`crate::silph_synth::spawn_silph_workers`] (triggered + /// by the first observation of a load-bearing VFS path such as + /// `dat/movie`), then reused — subsequent triggers are no-ops. + pub silph_synth_done: bool, + /// AUDIT-2.BF — VA of the synthesised silph::WorkerCtx. Zero before the + /// first spawn; set to the ctx base by `spawn_silph_workers`. Held on + /// the kernel state so future export hooks can find it (no caller does + /// yet — placeholder for round 19+ wiring). + pub silph_synth_ctx: u32, + /// AUDIT-2.BF — kernel handles for the 4 synthetic worker threads. + pub silph_synth_handles: [Option; 4], + /// AUDIT-2.BF — `ThreadRef` cache for the 4 synthetic workers. + pub silph_synth_refs: [Option; 4], } impl KernelState { @@ -369,6 +383,10 @@ impl KernelState { lr_trace_writer: None, dump_addrs: Vec::new(), dump_section: None, + silph_synth_done: false, + silph_synth_ctx: 0, + silph_synth_handles: [None; 4], + silph_synth_refs: [None; 4], }; crate::exports::register_exports(&mut state); crate::xam::register_exports(&mut state);