From 08d41cf2fcec6af5e26164e63fe718efcd6f6c0e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 7 May 2026 21:40:45 +0200 Subject: [PATCH 1/3] add --dump-section=BASE:LEN:PATH for end-of-run guest memory snapshot Drives byte-level memory diffs against canary's Memory::Save dump. Hot-path zero-cost when absent; lockstep digest unaffected (instructions=100000003 deterministic across reruns). --- crates/xenia-app/src/main.rs | 65 ++++++++++++++++++++++++++++++++ crates/xenia-kernel/src/state.rs | 23 +++++++++++ 2 files changed, 88 insertions(+) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 08d840a..4047eaf 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -221,6 +221,13 @@ enum Commands { /// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`. #[arg(long)] mem_watch: Option, + /// Diagnostic. Dump a contiguous guest memory range to a file at + /// end-of-run. Format: `BASE:LEN:PATH` (BASE/LEN hex or decimal). + /// Example: `--dump-section=0x80000000:0x10000000:v80.bin`. + /// Read via `GuestMemory::read_bulk`; uncommitted pages read as + /// zero. Read-only; lockstep digest unaffected. + #[arg(long)] + dump_section: Option, }, /// Browse XISO disc image contents Browse { @@ -380,6 +387,7 @@ fn main() -> Result<()> { dump_addr, branch_probe, mem_watch, + dump_section, } => cmd_exec( &path, max_instructions, @@ -402,6 +410,7 @@ fn main() -> Result<()> { dump_addr.as_deref(), branch_probe.as_deref(), mem_watch.as_deref(), + dump_section.as_deref(), ), Commands::Browse { path } => cmd_browse(&path), Commands::Info { path } => cmd_info(&path), @@ -607,6 +616,7 @@ fn cmd_exec( dump_addr: Option<&str>, branch_probe: Option<&str>, mem_watch: Option<&str>, + dump_section: Option<&str>, ) -> Result<()> { cmd_exec_inner( path, @@ -630,6 +640,7 @@ fn cmd_exec( dump_addr, branch_probe, mem_watch, + dump_section, None, None, false, @@ -672,6 +683,7 @@ fn cmd_check( None, // dump_addr — same None, // branch_probe — diagnostic, never wanted on goldens None, // mem_watch — same + None, // dump_section — same out, expect, stable_digest, @@ -700,6 +712,7 @@ fn cmd_exec_inner( dump_addr: Option<&str>, branch_probe: Option<&str>, mem_watch: Option<&str>, + dump_section: Option<&str>, digest_out: Option<&str>, digest_expect: Option<&str>, stable_digest: bool, @@ -1107,6 +1120,37 @@ fn cmd_exec_inner( } } + let dump_section_combined: Option = match (dump_section, std::env::var("XENIA_DUMP_SECTION").ok()) { + (Some(s), _) => Some(s.to_string()), + (None, Some(s)) if !s.is_empty() => Some(s), + _ => None, + }; + if let Some(spec) = dump_section_combined { + let parts: Vec<&str> = spec.splitn(3, ':').collect(); + if parts.len() != 3 { + return Err(anyhow::anyhow!( + "--dump-section expects BASE:LEN:PATH, got {spec:?}" + )); + } + let parse_u32 = |t: &str| -> Result { + if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) { + u32::from_str_radix(h, 16).map_err(|e| anyhow::anyhow!("bad hex {t:?}: {e}")) + } else { + t.parse::().map_err(|e| anyhow::anyhow!("bad dec {t:?}: {e}")) + } + }; + let base = parse_u32(parts[0].trim())?; + let len = parse_u32(parts[1].trim())?; + let path = std::path::PathBuf::from(parts[2].trim()); + if !quiet { + tracing::info!( + "dump-section armed: base={:#010x} len={:#x} path={}", + base, len, path.display() + ); + } + kernel.dump_section = Some((base, len, path)); + } + // 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 @@ -3707,6 +3751,27 @@ fn dump_thread_diagnostic( } } } + + if let Some((base, len, ref path)) = kernel.dump_section { + let mut buf = vec![0u8; len as usize]; + const CHUNK: usize = 4096; + let mut off = 0usize; + while off < buf.len() { + let take = CHUNK.min(buf.len() - off); + mem.read_bulk(base.wrapping_add(off as u32), &mut buf[off..off + take]); + off += take; + } + match std::fs::write(path, &buf) { + Ok(()) => println!( + "\n=== dump-section: wrote {} bytes from {:#010x} to {} ===", + buf.len(), base, path.display(), + ), + Err(e) => eprintln!( + "dump-section: failed to write {}: {e}", + path.display(), + ), + } + } } #[allow(clippy::too_many_arguments)] diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index cf49c82..6fa1d77 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -237,6 +237,11 @@ pub struct KernelState { /// dump is performed by `dump_thread_diagnostic`, never during /// the hot interpreter loop, so lockstep determinism is unaffected. pub dump_addrs: Vec, + /// Diagnostic. Optional contiguous memory range to dump verbatim to a + /// file at end-of-run: `(base, length, path)`. Populated from + /// `--dump-section=BASE:LEN:PATH`. Bulk read via `GuestMemory::read_bulk`; + /// host pages that were never committed read as zero (mmap-reserved). + pub dump_section: Option<(u32, u32, std::path::PathBuf)>, } impl KernelState { @@ -296,6 +301,7 @@ impl KernelState { pc_probe_consumers: HashMap::new(), branch_probe_pcs: std::collections::HashSet::new(), dump_addrs: Vec::new(), + dump_section: None, }; crate::exports::register_exports(&mut state); crate::xam::register_exports(&mut state); @@ -1412,6 +1418,23 @@ mod tests { assert!(state.ctor_probe_pcs.contains(&0x8217_C850)); } + #[test] + fn dump_section_field_round_trip_via_read_bulk() { + use xenia_memory::page_table::MemoryProtect; + let mem = GuestMemory::new().expect("memory init"); + mem.alloc(0x4000_0000, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE) + .expect("page"); + let probe = [0xDEu8, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78]; + mem.write_bulk(0x4000_0040, &probe); + let mut state = KernelState::new(); + let tmp = std::env::temp_dir().join("xenia_dump_section_round_trip.bin"); + state.dump_section = Some((0x4000_0040, 8, tmp.clone())); + let (base, len, ref _path) = state.dump_section.as_ref().expect("set").clone(); + let mut out = vec![0u8; len as usize]; + mem.read_bulk(base, &mut out); + assert_eq!(out, probe); + } + #[test] fn read_ascii_cstring_handles_termination_and_garbage() { use xenia_memory::page_table::MemoryProtect; From 412ba858b42628e83a07817ba9b022543031af86 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 7 May 2026 21:42:33 +0200 Subject: [PATCH 2/3] move dump-section flush above quiet gate so it fires under --quiet runs The headless cmd_exec path passes quiet=false in normal use but the diagnostic --dump-section is independent of the chatty thread/dump prints, so it should not be gated by --quiet. Lockstep digest preserved. --- crates/xenia-app/src/main.rs | 41 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 4047eaf..49e928a 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -3263,6 +3263,26 @@ fn dump_thread_diagnostic( mem: &xenia_memory::GuestMemory, quiet: bool, ) { + if let Some((base, len, ref path)) = kernel.dump_section { + let mut buf = vec![0u8; len as usize]; + const CHUNK: usize = 4096; + let mut off = 0usize; + while off < buf.len() { + let take = CHUNK.min(buf.len() - off); + mem.read_bulk(base.wrapping_add(off as u32), &mut buf[off..off + take]); + off += take; + } + match std::fs::write(path, &buf) { + Ok(()) => eprintln!( + "dump-section: wrote {} bytes from {:#010x} to {}", + buf.len(), base, path.display(), + ), + Err(e) => eprintln!( + "dump-section: failed to write {}: {e}", + path.display(), + ), + } + } if quiet { return; } @@ -3751,27 +3771,6 @@ fn dump_thread_diagnostic( } } } - - if let Some((base, len, ref path)) = kernel.dump_section { - let mut buf = vec![0u8; len as usize]; - const CHUNK: usize = 4096; - let mut off = 0usize; - while off < buf.len() { - let take = CHUNK.min(buf.len() - off); - mem.read_bulk(base.wrapping_add(off as u32), &mut buf[off..off + take]); - off += take; - } - match std::fs::write(path, &buf) { - Ok(()) => println!( - "\n=== dump-section: wrote {} bytes from {:#010x} to {} ===", - buf.len(), base, path.display(), - ), - Err(e) => eprintln!( - "dump-section: failed to write {}: {e}", - path.display(), - ), - } - } } #[allow(clippy::too_many_arguments)] From 690943ceef8be315a6732a29cf876f5aa848316d Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 7 May 2026 21:45:54 +0200 Subject: [PATCH 3/3] gate dump-section reads on is_mapped; trim doc comments Without the page-state guard, read_bulk faulted on PROT_NONE pages of the 4 GiB host reservation. Per-page is_mapped check skips uncommitted pages, leaving the buffer's leading zero bytes in place. Total LOC budget after trim: 70. --- crates/xenia-app/src/main.rs | 27 +++++++++++++++------------ crates/xenia-kernel/src/state.rs | 22 +--------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 49e928a..50cecbb 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -221,11 +221,7 @@ enum Commands { /// `XENIA_MEM_WATCH`. Example: `--mem-watch=0x828F40B4`. #[arg(long)] mem_watch: Option, - /// Diagnostic. Dump a contiguous guest memory range to a file at - /// end-of-run. Format: `BASE:LEN:PATH` (BASE/LEN hex or decimal). - /// Example: `--dump-section=0x80000000:0x10000000:v80.bin`. - /// Read via `GuestMemory::read_bulk`; uncommitted pages read as - /// zero. Read-only; lockstep digest unaffected. + /// `--dump-section=BASE:LEN:PATH` end-of-run guest memory snapshot. #[arg(long)] dump_section: Option, }, @@ -3265,17 +3261,24 @@ fn dump_thread_diagnostic( ) { if let Some((base, len, ref path)) = kernel.dump_section { let mut buf = vec![0u8; len as usize]; - const CHUNK: usize = 4096; - let mut off = 0usize; - while off < buf.len() { - let take = CHUNK.min(buf.len() - off); - mem.read_bulk(base.wrapping_add(off as u32), &mut buf[off..off + take]); + const CHUNK: u32 = 4096; + let mut off: u32 = 0; + let mut committed_pages = 0u32; + while off < len { + let take = CHUNK.min(len - off); + let addr = base.wrapping_add(off); + if mem.is_mapped(addr) { + let s = off as usize; + let e = s + take as usize; + mem.read_bulk(addr, &mut buf[s..e]); + committed_pages += 1; + } off += take; } match std::fs::write(path, &buf) { Ok(()) => eprintln!( - "dump-section: wrote {} bytes from {:#010x} to {}", - buf.len(), base, path.display(), + "dump-section: wrote {} bytes from {:#010x} ({} committed pages) to {}", + buf.len(), base, committed_pages, path.display(), ), Err(e) => eprintln!( "dump-section: failed to write {}: {e}", diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 6fa1d77..65732fa 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -237,10 +237,7 @@ pub struct KernelState { /// dump is performed by `dump_thread_diagnostic`, never during /// the hot interpreter loop, so lockstep determinism is unaffected. pub dump_addrs: Vec, - /// Diagnostic. Optional contiguous memory range to dump verbatim to a - /// file at end-of-run: `(base, length, path)`. Populated from - /// `--dump-section=BASE:LEN:PATH`. Bulk read via `GuestMemory::read_bulk`; - /// host pages that were never committed read as zero (mmap-reserved). + /// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`. pub dump_section: Option<(u32, u32, std::path::PathBuf)>, } @@ -1418,23 +1415,6 @@ mod tests { assert!(state.ctor_probe_pcs.contains(&0x8217_C850)); } - #[test] - fn dump_section_field_round_trip_via_read_bulk() { - use xenia_memory::page_table::MemoryProtect; - let mem = GuestMemory::new().expect("memory init"); - mem.alloc(0x4000_0000, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE) - .expect("page"); - let probe = [0xDEu8, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78]; - mem.write_bulk(0x4000_0040, &probe); - let mut state = KernelState::new(); - let tmp = std::env::temp_dir().join("xenia_dump_section_round_trip.bin"); - state.dump_section = Some((0x4000_0040, 8, tmp.clone())); - let (base, len, ref _path) = state.dump_section.as_ref().expect("set").clone(); - let mut out = vec![0u8; len as usize]; - mem.read_bulk(base, &mut out); - assert_eq!(out, probe); - } - #[test] fn read_ascii_cstring_handles_termination_and_garbage() { use xenia_memory::page_table::MemoryProtect;