Compare commits
6 Commits
iterate-2B
...
iterate-2B
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40f208ea4e | ||
|
|
8683fb59ed | ||
|
|
b5885b8560 | ||
|
|
9340ff4592 | ||
|
|
bcd018659b | ||
|
|
09e59e09b7 |
@@ -242,6 +242,44 @@ enum Commands {
|
||||
/// line). Stdout when omitted.
|
||||
#[arg(long)]
|
||||
lr_trace_out: Option<String>,
|
||||
/// AUDIT-2BF — comma-separated list of guest PCs (hex, no `0x`
|
||||
/// prefix required) to capture as one-line `AUDIT-PC-PROBE`
|
||||
/// records on every fire. Designed for the silph init chain
|
||||
/// virtual-dispatch site at `sub_82172BA0+0x1E8` (PC
|
||||
/// `0x82172D88`, a `bctrl` after a 3-deep vtable-slot-6 load).
|
||||
/// Each record carries (pc, tid, hw, cycle, lr, r3, r11) plus
|
||||
/// four guest-memory dereferences off r3: `[r3+0]` (vtable),
|
||||
/// `[[r3+0]+24]` (slot 6 method = bctrl target), `[r3+0x0C]`
|
||||
/// (auxiliary handle), `[r3+0x30]` (embedded sub-object vtable).
|
||||
/// Compares directly against canary's round-9 capture:
|
||||
/// r3=0xBCCC52C0, [r3+0]=0x820A3644, slot6=sub_821B55D8,
|
||||
/// [r3+0xC]=0xF80000D8, [r3+0x30]=0x820A1870. Read-only;
|
||||
/// lockstep digest unaffected. Settable via
|
||||
/// `XENIA_AUDIT_PC_PROBE`. Example:
|
||||
/// `--audit-pc-probe-hex=82172D88,82172D80`.
|
||||
#[arg(long)]
|
||||
audit_pc_probe_hex: Option<String>,
|
||||
/// AUDIT-2BF round 14 — guest VA (hex, optional `0x` prefix) to
|
||||
/// dereference 3 deep on every `--audit-pc-probe-hex` fire.
|
||||
/// Emits a paired `AUDIT-MEM-READ` line with the singleton value,
|
||||
/// vtable, vtable[0] (= first virtual method, the bctrl target
|
||||
/// at `0x822F1B4C`), and vtable[24] (= slot 6 = canary's silph
|
||||
/// chain target `sub_821B55D8`). Compare ours vs canary to
|
||||
/// determine whether the bctrl dispatches to the same function
|
||||
/// or a different one. Read-only; lockstep digest unaffected.
|
||||
/// Settable via `XENIA_AUDIT_MEM_READ`. Example:
|
||||
/// `--audit-mem-read-hex=828E1F08`.
|
||||
#[arg(long)]
|
||||
audit_mem_read_hex: Option<String>,
|
||||
/// AUDIT-052 — number of bytes (4-byte aligned, max 256) to
|
||||
/// dump from `r3` on every `--audit-pc-probe-hex` fire. Emits a
|
||||
/// paired `AUDIT-R3-DUMP` line with the u32 lanes. Designed for
|
||||
/// the 80-byte stack-local struct at `sub_82452DC0` (`r31+96`)
|
||||
/// when probing `sub_8245B000` entry — where `r3` IS the struct
|
||||
/// pointer. Read-only; lockstep digest unaffected. Settable via
|
||||
/// `XENIA_AUDIT_R3_DUMP_BYTES`. Example: `--audit-r3-dump-bytes=80`.
|
||||
#[arg(long)]
|
||||
audit_r3_dump_bytes: Option<u32>,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -405,6 +443,9 @@ fn main() -> Result<()> {
|
||||
probe_db,
|
||||
lr_trace,
|
||||
lr_trace_out,
|
||||
audit_pc_probe_hex,
|
||||
audit_mem_read_hex,
|
||||
audit_r3_dump_bytes,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -431,6 +472,9 @@ fn main() -> Result<()> {
|
||||
probe_db.as_deref(),
|
||||
lr_trace.as_deref(),
|
||||
lr_trace_out.as_deref(),
|
||||
audit_pc_probe_hex.as_deref(),
|
||||
audit_mem_read_hex.as_deref(),
|
||||
audit_r3_dump_bytes,
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
@@ -662,6 +706,9 @@ fn cmd_exec(
|
||||
probe_db: Option<&str>,
|
||||
lr_trace: Option<&str>,
|
||||
lr_trace_out: Option<&str>,
|
||||
audit_pc_probe_hex: Option<&str>,
|
||||
audit_mem_read_hex: Option<&str>,
|
||||
audit_r3_dump_bytes: Option<u32>,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -689,6 +736,9 @@ fn cmd_exec(
|
||||
probe_db,
|
||||
lr_trace,
|
||||
lr_trace_out,
|
||||
audit_pc_probe_hex,
|
||||
audit_mem_read_hex,
|
||||
audit_r3_dump_bytes,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -735,6 +785,9 @@ fn cmd_check(
|
||||
None, // probe_db — same
|
||||
None, // lr_trace — same
|
||||
None, // lr_trace_out — same
|
||||
None, // audit_pc_probe_hex — diagnostic, never wanted on goldens
|
||||
None, // audit_mem_read_hex — same
|
||||
None, // audit_r3_dump_bytes — same
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
@@ -767,6 +820,9 @@ fn cmd_exec_inner(
|
||||
probe_db: Option<&str>,
|
||||
lr_trace: Option<&str>,
|
||||
lr_trace_out: Option<&str>,
|
||||
audit_pc_probe_hex: Option<&str>,
|
||||
audit_mem_read_hex: Option<&str>,
|
||||
audit_r3_dump_bytes: Option<u32>,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
@@ -1167,6 +1223,84 @@ fn cmd_exec_inner(
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIT-2BF — `--audit-pc-probe-hex=82172D88,...`. Bare-hex tokens
|
||||
// (with or without `0x` prefix). Parses every comma-separated entry
|
||||
// as a u32 PC and inserts into `kernel.audit_pc_probe_pcs`. Empty
|
||||
// set is the hot-path no-op (single is_empty() check).
|
||||
let audit_pc_probe_combined: Option<String> = match (
|
||||
audit_pc_probe_hex, std::env::var("XENIA_AUDIT_PC_PROBE").ok(),
|
||||
) {
|
||||
(Some(s), _) => Some(s.to_string()),
|
||||
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(list) = audit_pc_probe_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let hex = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")).unwrap_or(token);
|
||||
let pc = u32::from_str_radix(hex, 16)
|
||||
.map_err(|e| anyhow::anyhow!("--audit-pc-probe-hex {token:?}: {e}"))?;
|
||||
kernel.audit_pc_probe_pcs.insert(pc);
|
||||
}
|
||||
if !quiet && !kernel.audit_pc_probe_pcs.is_empty() {
|
||||
let mut pcs: Vec<u32> = kernel.audit_pc_probe_pcs.iter().copied().collect();
|
||||
pcs.sort_unstable();
|
||||
let strs: Vec<String> = pcs.iter().map(|p| format!("{p:#010x}")).collect();
|
||||
tracing::info!(
|
||||
"audit-pc-probe armed: {} ({})",
|
||||
kernel.audit_pc_probe_pcs.len(),
|
||||
strs.join(", "),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIT-2BF round 14 — `--audit-mem-read-hex=828E1F08`. Single
|
||||
// hex VA (optional `0x` prefix). Stored on `kernel.audit_mem_read_addr`.
|
||||
// Paired with `audit_pc_probe_pcs`: on every probe fire, the kernel
|
||||
// emits a second `AUDIT-MEM-READ` line dereferencing 3 deep so we can
|
||||
// resolve vtable[0] / vtable[24] at the singleton.
|
||||
let audit_mem_read_combined: Option<String> = match (
|
||||
audit_mem_read_hex, std::env::var("XENIA_AUDIT_MEM_READ").ok(),
|
||||
) {
|
||||
(Some(s), _) => Some(s.to_string()),
|
||||
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(tok) = audit_mem_read_combined {
|
||||
let tok = tok.trim();
|
||||
if !tok.is_empty() {
|
||||
let hex = tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")).unwrap_or(tok);
|
||||
let addr = u32::from_str_radix(hex, 16)
|
||||
.map_err(|e| anyhow::anyhow!("--audit-mem-read-hex {tok:?}: {e}"))?;
|
||||
kernel.audit_mem_read_addr = Some(addr);
|
||||
if !quiet {
|
||||
tracing::info!("audit-mem-read armed: {:#010x}", addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIT-052 — `--audit-r3-dump-bytes=80`. When set, every
|
||||
// `--audit-pc-probe-hex` fire emits a paired `AUDIT-R3-DUMP` line
|
||||
// with N bytes from `r3` (4-byte aligned, capped at 256). Sized for
|
||||
// the 80-byte stack-local struct at `sub_82452DC0`'s `r31+96` —
|
||||
// probe `sub_8245B000` entry where `r3 == parent's r31+96`.
|
||||
let audit_r3_dump_combined: Option<u32> = match (
|
||||
audit_r3_dump_bytes, std::env::var("XENIA_AUDIT_R3_DUMP_BYTES").ok(),
|
||||
) {
|
||||
(Some(n), _) => Some(n),
|
||||
(None, Some(s)) if !s.is_empty() => Some(
|
||||
s.parse::<u32>().map_err(|e| anyhow::anyhow!("--audit-r3-dump-bytes {s:?}: {e}"))?,
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(n) = audit_r3_dump_combined {
|
||||
if n > 0 {
|
||||
kernel.audit_r3_dump_bytes = Some(n);
|
||||
if !quiet {
|
||||
tracing::info!("audit-r3-dump armed: {} bytes", n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic. Parse `--dump-addr=0x828F3D08,...` (or
|
||||
// `XENIA_DUMP_ADDR=...`) into `kernel.dump_addrs`. The contents
|
||||
// are dumped at end-of-run by `dump_thread_diagnostic`. Pure
|
||||
@@ -2242,6 +2376,7 @@ fn worker_prologue(
|
||||
// the helper, no overhead on the hot path.
|
||||
kernel.fire_ctor_probe_if_match(hw_id, mem);
|
||||
kernel.fire_branch_probe_if_match(hw_id);
|
||||
kernel.fire_audit_pc_probe_if_match(hw_id, mem);
|
||||
kernel.fire_lr_trace_if_match(hw_id);
|
||||
|
||||
if mem.has_mem_watch() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
280
crates/xenia-kernel/src/silph_synth.rs
Normal file
280
crates/xenia-kernel/src/silph_synth.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
//! 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-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). Round 21 retains the
|
||||
/// embedded sub-object but drops the synthesized vtable (we now point at
|
||||
/// canary's real XEX-resident sub-vtable directly), so the 0x500..0x580
|
||||
/// region is unused but harmless.
|
||||
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;
|
||||
|
||||
/// XEX `.rdata` VA of canary's real sub-object vtable (audit-059 round 21).
|
||||
/// Discovered by:
|
||||
/// 1. Probing canary at `pc=0x82506B08` (= `sub_82506B08`, method 35 of
|
||||
/// the WorkerCtx vtable, the first sub-object method called by every
|
||||
/// `sub_82506528/58/88/B8` worker entry).
|
||||
/// 2. Capturing `[ctx+0x2C]` from the JIT-prolog dump (= sub-object VA
|
||||
/// in canary's heap).
|
||||
/// 3. Re-running with `--audit_jit_prolog_mem_dump=<sub-obj VA>` to read
|
||||
/// `[sub-object + 0]` = sub-vtable VA = **`0x8200A168`**.
|
||||
/// PE inspection confirms slot 15 (called via `[r11+0x3C]` at
|
||||
/// `sub_82506B08+0x44`) = `sub_824FCCC8` and slot 17 (`[r11+0x44]` at
|
||||
/// `sub_82506B08+0x70`) = `sub_824FCE38`. Both are real game methods in
|
||||
/// the same `.text` region as the rest of the worker dispatch surface.
|
||||
const SILPH_SUB_VTABLE_SOURCE_VA: u32 = 0x8200_A168;
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
// +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;
|
||||
mem.write_u32(ctx + 0x2C, subobj_ptr);
|
||||
mem.write_u32(ctx + 0x30, SILPH_CTX_FIELD_30_CONST);
|
||||
|
||||
// ---- Embedded sub-object at +0x300 ----
|
||||
// Round-21 pivot: instead of synthesising a stub vtable that returns
|
||||
// NULL from every slot, point `[sub_object + 0]` directly at canary's
|
||||
// real XEX-resident sub-vtable VA. The vtable bytes are part of the
|
||||
// same static image both engines map, so referring to it costs zero
|
||||
// guest memory and gives the workers a working virtual-method surface
|
||||
// (slot 15 = sub_824FCCC8, slot 17 = sub_824FCE38, plus 29 other real
|
||||
// methods). Round-19 disassembly shows worker bodies only touch the
|
||||
// sub-object's vtable; the rest of the sub-object is opaque so we
|
||||
// leave it zero-filled.
|
||||
mem.write_u32(subobj_ptr, SILPH_SUB_VTABLE_SOURCE_VA);
|
||||
|
||||
// ---- 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)
|
||||
}
|
||||
@@ -244,6 +244,41 @@ pub struct KernelState {
|
||||
/// Distinct from `ctor_probe_pcs` because that helper emits 8
|
||||
/// frames of back-chain per hit — too noisy for branch tracing.
|
||||
pub branch_probe_pcs: std::collections::HashSet<u32>,
|
||||
/// AUDIT-2BF — diagnostic. PCs at which to emit a structured one-line
|
||||
/// `AUDIT-PC-PROBE` record on every fire, designed for the silph init
|
||||
/// chain virtual-dispatch site at `sub_82172BA0+0x1E8` (PC
|
||||
/// `0x82172D88`, a `bctrl` after a 3-deep load of vtable slot 6). The
|
||||
/// emitted line carries (pc, tid, hw, cycle, lr, r3, r11) plus four
|
||||
/// guest-memory dereferences off `r3`: `[r3+0]` (vtable), `[[r3+0]+24]`
|
||||
/// (slot 6 method pointer = the bctrl target), `[r3+0x0C]` (audit-059
|
||||
/// round-9 canary-known auxiliary handle `0xF80000D8`), and `[r3+0x30]`
|
||||
/// (canary-known embedded sub-object vtable `0x820A1870`). Distinct
|
||||
/// from `branch_probe_pcs` because that helper only logs registers (no
|
||||
/// memory) and from `lr_trace_pcs` because that emits JSON intended
|
||||
/// for canary diffing, not the four hard-coded indirect dereferences
|
||||
/// needed here. Read-only — no guest state mutation. Lockstep
|
||||
/// digest unaffected. Settable via `--audit-pc-probe-hex` /
|
||||
/// `XENIA_AUDIT_PC_PROBE`.
|
||||
pub audit_pc_probe_pcs: std::collections::HashSet<u32>,
|
||||
/// AUDIT-2BF round 14 — diagnostic. Optional guest VA. When set, each
|
||||
/// `AUDIT-PC-PROBE` fire emits a paired `AUDIT-MEM-READ` line with
|
||||
/// `addr`, `*addr` (singleton value), `**addr` (vtable), `***addr+0`
|
||||
/// (vtable[0] = first virtual method), and `***addr+24` (vtable[6]
|
||||
/// in 4-byte stride = slot 6 = silph chain bctrl target). Three-deep
|
||||
/// dereference to resolve the vtable[0] target at the bctrl site
|
||||
/// `0x822F1B4C` inside `sub_822F1AA8`. Read-only; lockstep digest
|
||||
/// unaffected. Settable via `--audit-mem-read-hex` /
|
||||
/// `XENIA_AUDIT_MEM_READ`.
|
||||
pub audit_mem_read_addr: Option<u32>,
|
||||
/// AUDIT-052 — diagnostic. When set, each `AUDIT-PC-PROBE` fire
|
||||
/// additionally emits an `AUDIT-R3-DUMP` line with N bytes of guest
|
||||
/// memory dumped from `r3` as `u32` lanes (4-byte aligned only).
|
||||
/// Sized for audit-051's 80-byte stack-local struct at `r31+96`
|
||||
/// inside `sub_82452DC0` (probe `sub_8245B000` entry where
|
||||
/// `r3 == parent's r31+96`). Read-only; lockstep digest unaffected.
|
||||
/// Settable via `--audit-r3-dump-bytes` /
|
||||
/// `XENIA_AUDIT_R3_DUMP_BYTES`.
|
||||
pub audit_r3_dump_bytes: Option<u32>,
|
||||
/// M12 — diagnostic. PCs at which to emit a structured JSONL record
|
||||
/// per fire, designed for diffing against xenia-canary's
|
||||
/// `--log_lr_on_pc` patch output. Each line carries
|
||||
@@ -264,6 +299,20 @@ pub struct KernelState {
|
||||
pub dump_addrs: Vec<u32>,
|
||||
/// `--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<u32>; 4],
|
||||
/// AUDIT-2.BF — `ThreadRef` cache for the 4 synthetic workers.
|
||||
pub silph_synth_refs: [Option<xenia_cpu::ThreadRef>; 4],
|
||||
}
|
||||
|
||||
impl KernelState {
|
||||
@@ -327,10 +376,17 @@ impl KernelState {
|
||||
ctor_probe_pcs: std::collections::HashSet::new(),
|
||||
pc_probe_consumers: HashMap::new(),
|
||||
branch_probe_pcs: std::collections::HashSet::new(),
|
||||
audit_pc_probe_pcs: std::collections::HashSet::new(),
|
||||
audit_mem_read_addr: None,
|
||||
audit_r3_dump_bytes: None,
|
||||
lr_trace_pcs: std::collections::HashSet::new(),
|
||||
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);
|
||||
@@ -797,6 +853,91 @@ impl KernelState {
|
||||
);
|
||||
}
|
||||
|
||||
/// AUDIT-2BF — diagnostic. If the live PC for HW slot `hw_id` is in
|
||||
/// `self.audit_pc_probe_pcs`, emit a single one-line
|
||||
/// `AUDIT-PC-PROBE` record with (pc, tid, hw, cycle, lr, r3, r11)
|
||||
/// plus four guest-memory dereferences off r3: `[r3+0]` (vtable),
|
||||
/// `[[r3+0]+24]` (slot 6 method = bctrl target), `[r3+0x0C]`
|
||||
/// (auxiliary handle field), `[r3+0x30]` (embedded sub-object
|
||||
/// vtable field). Tuned for the silph init chain virtual-dispatch
|
||||
/// site at `sub_82172BA0+0x1E8` (PC `0x82172D88`).
|
||||
///
|
||||
/// Read-only. No guest-state mutation; lockstep digest unaffected.
|
||||
/// Empty set is the common case → single `is_empty()` test on the
|
||||
/// hot path.
|
||||
pub fn fire_audit_pc_probe_if_match(&self, hw_id: u8, mem: &GuestMemory) {
|
||||
if self.audit_pc_probe_pcs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.scheduler.ctx(hw_id);
|
||||
let pc = ctx.pc;
|
||||
if !self.audit_pc_probe_pcs.contains(&pc) {
|
||||
return;
|
||||
}
|
||||
let tid = self.scheduler.tid(hw_id).unwrap_or(0);
|
||||
let r3 = ctx.gpr[3] as u32;
|
||||
let r11 = ctx.gpr[11] as u32;
|
||||
let lr = ctx.lr as u32;
|
||||
let cycle = ctx.cycle_count;
|
||||
// Memory dereferences. Guest pointers may be unmapped/garbage;
|
||||
// `read_u32` returns 0 for unmapped pages (heap.rs:510 returns
|
||||
// a default), so an all-zero block in the output reliably
|
||||
// indicates an invalid `r3`.
|
||||
let vtable = mem.read_u32(r3);
|
||||
let slot6_method = if vtable != 0 {
|
||||
mem.read_u32(vtable.wrapping_add(24))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let aux_handle = mem.read_u32(r3.wrapping_add(0x0C));
|
||||
let sub_vt = mem.read_u32(r3.wrapping_add(0x30));
|
||||
println!(
|
||||
"AUDIT-PC-PROBE pc={:#010x} tid={} hw={} cycle={} lr={:#010x} r3={:#010x} r11={:#010x} \
|
||||
[r3+0]={:#010x} [[r3+0]+24]={:#010x} [r3+0x0C]={:#010x} [r3+0x30]={:#010x}",
|
||||
pc, tid, hw_id, cycle, lr, r3, r11,
|
||||
vtable, slot6_method, aux_handle, sub_vt,
|
||||
);
|
||||
// AUDIT-2BF round 14 — paired memory-read. When
|
||||
// `audit_mem_read_addr` is set, dereference 3 deep: singleton
|
||||
// pointer → vtable → vtable[0] / vtable[24]. Defensively
|
||||
// null-checks each level. `read_u32` returns 0 for unmapped
|
||||
// pages so all-zero output is the unmapped/uninitialized
|
||||
// signature.
|
||||
if let Some(addr) = self.audit_mem_read_addr {
|
||||
let val = mem.read_u32(addr);
|
||||
let vt = if val != 0 { mem.read_u32(val) } else { 0 };
|
||||
let m0 = if vt != 0 { mem.read_u32(vt) } else { 0 };
|
||||
let m6 = if vt != 0 { mem.read_u32(vt.wrapping_add(24)) } else { 0 };
|
||||
println!(
|
||||
"AUDIT-MEM-READ addr={:#010x} val={:#010x} vtable={:#010x} \
|
||||
vtable[0]={:#010x} vtable[24]={:#010x} pc={:#010x} tid={} cycle={}",
|
||||
addr, val, vt, m0, m6, pc, tid, cycle,
|
||||
);
|
||||
}
|
||||
// AUDIT-052 — dump N bytes of guest memory from r3 as u32 lanes
|
||||
// when `audit_r3_dump_bytes` is set. Sized for the 80-byte
|
||||
// stack-local struct at sub_82452DC0's `r31+96` (probe is
|
||||
// sub_8245B000 entry where r3 IS the struct ptr). Output
|
||||
// format: `AUDIT-R3-DUMP pc=… r3=… +0x00=… +0x04=… …`.
|
||||
if let Some(n) = self.audit_r3_dump_bytes {
|
||||
let n = n.min(256) & !3u32; // cap 256B, 4-byte align
|
||||
let mut out = String::with_capacity(64 + (n as usize) * 16);
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(
|
||||
&mut out,
|
||||
"AUDIT-R3-DUMP pc={:#010x} tid={} cycle={} r3={:#010x}",
|
||||
pc, tid, cycle, r3,
|
||||
);
|
||||
let mut off: u32 = 0;
|
||||
while off < n {
|
||||
let v = mem.read_u32(r3.wrapping_add(off));
|
||||
let _ = write!(&mut out, " +0x{:02x}={:#010x}", off, v);
|
||||
off = off.wrapping_add(4);
|
||||
}
|
||||
println!("{}", out);
|
||||
}
|
||||
}
|
||||
|
||||
/// M12 — diagnostic. If the live PC for HW slot `hw_id` is in
|
||||
/// `self.lr_trace_pcs`, emit one JSONL record. Format mirrors what
|
||||
/// xenia-canary's `--log_lr_on_pc` patch emits, plus the cycle
|
||||
|
||||
Reference in New Issue
Block a user