[2.BF] Synthetic silph::WorkerCtx spawn (round 18 — opt-in landing)
Adds infrastructure to synthesise the silph::WorkerCtx that AUDIT-058/059 identified as never reached by ours' static-init chain (real chain entry sits in audit-059 round 9's wrong-vtable wedge at sub_82172BA0+0x1E8). Ctx layout follows round 5's live hexdump from canary: +0x00 vtable = 0x8200A1E8 +0x04 self +0x08 intrusive list head -> self +0x0C init flag = 1 +0x10 packed byte field +0x18 2x float ~1.0 (UI rates) +0x24 flag = 1 +0x28..+0x30 3x foreign-arena pointers (left NULL — see below) +0x54..+0x84 4x X_KEVENT auto-reset, state=0 +0x94..+0xC4 4x X_KEVENT manual-reset, state=1 (pre-signaled) +0x210..+0x250 4-entry intrusive work-ring, empty Worker spawn mirrors AUDIT-048's audio-worker pattern in xaudio_register_render_driver: per-worker allocate_thread_image + state.scheduler.spawn with r3 = ctx_ptr. Trigger fires at the first dat/* VFS open (ours' earliest is dat/files.tbl), which is when canary runs the equivalent chain. ROUND 18 OUTCOME — opt-in only: With workers spawned Ready (XENIA_SILPH_SYNTH=1), boot CRASHES at cycle ~5.5M with PC=0 on hw=1, just after worker_3 (entry 0x825065B8) spawns. Per task constraints this is STOP-and-report: the ctx fields +0x28/+0x2C/+0x30 (foreign heap pointers — canary's 0x30057018, 0xBCE25640, 0xBE568F00, distinct arenas per audit-059 round 7) are left NULL, and the worker bodies plausibly dereference one of them. Synthesising those is a fresh investigation (round 19+). With workers spawned Suspended (XENIA_SILPH_SYNTH=suspend), boot completes normally (11 spawns, VdSwap=1, KeSetEvent=2, KeReleaseSemaphore=1 — matches default baseline). The ctx remains materialised in guest memory at the logged VA for downstream probing. Default (env var unset): no synth, no regression. Files: crates/xenia-kernel/src/silph_synth.rs (new, 225 LOC) crates/xenia-kernel/src/lib.rs (+1 LOC, register module) crates/xenia-kernel/src/exports.rs (+37 LOC, hook in open_vfs_file) crates/xenia-kernel/src/state.rs (+18 LOC, 4 silph_synth_* fields) Tests: cargo test --release --workspace = 765 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -980,6 +980,43 @@ fn open_vfs_file(
|
|||||||
// see a null handle later and trigger `XamShowDirtyDiscErrorUI`.
|
// see a null handle later and trigger `XamShowDirtyDiscErrorUI`.
|
||||||
let path = crate::path::object_attributes_to_vfs_path(mem, obj_attrs_ptr)
|
let path = crate::path::object_attributes_to_vfs_path(mem, obj_attrs_ptr)
|
||||||
.unwrap_or_default();
|
.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 path.is_empty() && obj_attrs_ptr == 0 {
|
||||||
if handle_out != 0 {
|
if handle_out != 0 {
|
||||||
mem.write_u32(handle_out, 0);
|
mem.write_u32(handle_out, 0);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod exports;
|
|||||||
pub mod interrupts;
|
pub mod interrupts;
|
||||||
pub mod objects;
|
pub mod objects;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
pub mod silph_synth;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
pub mod ui_bridge;
|
pub mod ui_bridge;
|
||||||
|
|||||||
225
crates/xenia-kernel/src/silph_synth.rs
Normal file
225
crates/xenia-kernel/src/silph_synth.rs
Normal file
@@ -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<u32> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -299,6 +299,20 @@ pub struct KernelState {
|
|||||||
pub dump_addrs: Vec<u32>,
|
pub dump_addrs: Vec<u32>,
|
||||||
/// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`.
|
/// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`.
|
||||||
pub dump_section: Option<(u32, u32, std::path::PathBuf)>,
|
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<u32>; 4],
|
||||||
|
/// AUDIT-2.BF — `ThreadRef` cache for the 4 synthetic workers.
|
||||||
|
pub silph_synth_refs: [Option<xenia_cpu::ThreadRef>; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KernelState {
|
impl KernelState {
|
||||||
@@ -369,6 +383,10 @@ impl KernelState {
|
|||||||
lr_trace_writer: None,
|
lr_trace_writer: None,
|
||||||
dump_addrs: Vec::new(),
|
dump_addrs: Vec::new(),
|
||||||
dump_section: None,
|
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::exports::register_exports(&mut state);
|
||||||
crate::xam::register_exports(&mut state);
|
crate::xam::register_exports(&mut state);
|
||||||
|
|||||||
Reference in New Issue
Block a user