feat(memory): --mem-watch=ADDR per-store writer trace

Adds an opt-in diagnostic that emits one tracing line per guest store
overlapping any armed byte address, naming the writer (tid, pc, lr)
plus old/new u32 lanes. Mirrors the --pc-probe / --branch-probe shape;
pc/lr are stamped from worker_prologue via a thread-local Cell, so
default runs (empty watch set) take a single is_empty() check on each
write. Lockstep digest preserved (instructions=100000003 across reruns,
sylpheed_n50m.json golden byte-identical).

Diagnostic infra only; no functional change. Used to identify producers
of dispatch-state writes for the audit-017 / audit-019 hunt.
This commit is contained in:
MechaCat02
2026-05-06 21:00:20 +02:00
parent cc54ca8e64
commit 978a6950d1
3 changed files with 219 additions and 2 deletions

View File

@@ -213,6 +213,14 @@ enum Commands {
/// `XENIA_BRANCH_PROBE`.
#[arg(long)]
branch_probe: Option<String>,
/// Diagnostic. Comma-separated guest byte addresses; on every
/// guest store that overlaps any listed byte, emit one
/// `MEM-WATCH` line at tracing target `mem_watch` with the
/// (tid, pc, lr) of the writer plus old/new u32 lanes.
/// Read-only; lockstep digest unaffected. Settable via
/// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`.
#[arg(long)]
mem_watch: Option<String>,
},
/// Browse XISO disc image contents
Browse {
@@ -371,6 +379,7 @@ fn main() -> Result<()> {
ctor_probe,
dump_addr,
branch_probe,
mem_watch,
} => cmd_exec(
&path,
max_instructions,
@@ -392,6 +401,7 @@ fn main() -> Result<()> {
ctor_probe.as_deref(),
dump_addr.as_deref(),
branch_probe.as_deref(),
mem_watch.as_deref(),
),
Commands::Browse { path } => cmd_browse(&path),
Commands::Info { path } => cmd_info(&path),
@@ -596,6 +606,7 @@ fn cmd_exec(
ctor_probe: Option<&str>,
dump_addr: Option<&str>,
branch_probe: Option<&str>,
mem_watch: Option<&str>,
) -> Result<()> {
cmd_exec_inner(
path,
@@ -618,6 +629,7 @@ fn cmd_exec(
ctor_probe,
dump_addr,
branch_probe,
mem_watch,
None,
None,
false,
@@ -659,6 +671,7 @@ fn cmd_check(
None, // ctor_probe — diagnostic, never wanted on goldens
None, // dump_addr — same
None, // branch_probe — diagnostic, never wanted on goldens
None, // mem_watch — same
out,
expect,
stable_digest,
@@ -686,6 +699,7 @@ fn cmd_exec_inner(
ctor_probe: Option<&str>,
dump_addr: Option<&str>,
branch_probe: Option<&str>,
mem_watch: Option<&str>,
digest_out: Option<&str>,
digest_expect: Option<&str>,
stable_digest: bool,
@@ -1058,6 +1072,41 @@ fn cmd_exec_inner(
}
}
let mem_watch_combined: Option<String> = match (mem_watch, std::env::var("XENIA_MEM_WATCH").ok()) {
(Some(s), _) => Some(s.to_string()),
(None, Some(s)) if !s.is_empty() => Some(s),
_ => None,
};
let mut mem_watch_addrs: Vec<u32> = Vec::new();
if let Some(list) = mem_watch_combined {
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let parsed = if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
u32::from_str_radix(hex, 16)
} else {
token.parse::<u32>()
};
match parsed {
Ok(addr) => mem_watch_addrs.push(addr),
Err(_) => {
return Err(anyhow::anyhow!(
"invalid address in --mem-watch: {token:?}"
));
}
}
}
if !quiet && !mem_watch_addrs.is_empty() {
let strs: Vec<String> = mem_watch_addrs
.iter()
.map(|a| format!("{a:#010x}"))
.collect();
tracing::info!(
"mem-watch armed: {} ({})",
mem_watch_addrs.len(),
strs.join(", ")
);
}
}
// Install the GPU register aperture MMIO region on the guest memory so
// any `0x7FC8xxxx` access routes to our atomic mailbox. Matches canary's
// `graphics_system.cc:141-144`. The callbacks capture Arc clones of the
@@ -1272,6 +1321,10 @@ fn cmd_exec_inner(
// M1.4 — wrap `mem` in an `Arc<GuestMemory>` after all init mutations
// are complete. The worker thread (if spawned below) holds its own
if !mem_watch_addrs.is_empty() {
mem.arm_mem_watch(mem_watch_addrs);
}
// Arc clone for the duration of the run; the CPU side passes
// `&*mem_arc` (= `&GuestMemory`) into `run_execution`. The
// trait-level invariant carrying this is correctness: writes are
@@ -1999,6 +2052,12 @@ fn worker_prologue(
kernel.fire_ctor_probe_if_match(hw_id, mem);
kernel.fire_branch_probe_if_match(hw_id);
if mem.has_mem_watch() {
let ctx = kernel.scheduler.ctx(hw_id);
let tid = kernel.scheduler.tid(hw_id).unwrap_or(0);
xenia_memory::set_writer_ctx(tid, ctx.pc, ctx.lr as u32);
}
// 1) Halt-sentinel check (per HW thread).
if pc == LR_HALT {
let injected_here = kernel.interrupts.saved.is_some()