# AUDIT-068 Session 1 — host-side memory-write watch (canary instrumentation) Date: 2026-05-19 ## Goal Capture which host C++ functions perform the writes to guest memory that ours never reproduces: 1. Vtable install at `0xBCE25340 = 0x8200A208` (and clone `0x8200A928`) — gates `sub_825070F0`. 2. Voice-struct field clear `[VOICE+0x164]` (value `0x00000000`, on guest-VA likely in heap `0x425xxxxx`). 3. Anything else surfaced. ## Write-path surface inventory (canary) ### A. `xe::store_and_swap` template family (`xenia-canary/src/xenia/base/memory.h:410-475`) - Sized specializations for T = int8/uint8/int16/uint16/int32/uint32/int64/uint64/float/double. - String specializations recurse to `store_and_swap` / ``. - Receives `void* mem` = HOST pointer; does `*p = byte_swap(value)`. - **This is the canonical path** for host-side typed writes to guest memory used by kernel-import handlers in `xboxkrnl_*.cc`. Confirmed wide use (16 kernel sub-modules call `store_and_swap` alone). - Vtable install (PPC `stw vptr,0(obj)` equivalent on host side) almost certainly uses `store_and_swap(host_ptr, vptr)`. ### B. `Memory::Zero/Fill/Copy` (`xenia/memory.cc:542-554`) - Use `std::memset`/`std::memcpy` directly via host pointer (after `TranslateVirtual`). - Wrappers for `RtlZeroMemory`, `RtlFillMemory`, `RtlMoveMemory`, `RtlCopyMemory`. - Bypass `store_and_swap` — must instrument separately if we want full coverage. - Voice-struct clears via `0x00000000` could plausibly come through here (RtlZeroMemory) or directly via `store_and_swap` (typed write). ### C. Direct guest writes via `*TranslateVirtual() = …` - Some sites cast and write through the host pointer directly without going through `store_and_swap`. - Lower coverage priority — start with A+B; add C only if first 2 don't catch our targets. ## Cvar design (mimics audit_67 pattern) Two new cvars in `xenia/cpu/cpu_flags.{h,cc}`: ```cpp DECLARE_string(audit_68_host_mem_watch_values); // CSV of u32 values (max 8) DECLARE_string(audit_68_host_mem_watch_addrs); // CSV of guest VAs or VA ranges (max 8) ``` Format examples: - Values: `--audit_68_host_mem_watch_values=0x8200A208,0x8200A928` - Addrs single: `--audit_68_host_mem_watch_addrs=0xBCE25340` - Addrs range: `--audit_68_host_mem_watch_addrs=0x42500000-0x42600000,0xBCE25340` Default empty → zero overhead. Sample log line (XELOGI): ``` AUDIT-068-HOST-WRITE guest_va=0xBCE25340 val=0x8200A208 sz=4 fn= host_ns=10123456789 tid=N ``` `fn=` is filled by the caller (each `store_and_swap` specialization passes `__FUNCTION__` or a tag). We can't get a real backtrace cheaply across MSVC; we instead instrument the high-fanout entry points (kernel-import handlers, `Memory::Zero/Fill/Copy`) with a string tag. For Session 1, capture is sufficient with just template name + caller tag. ## Implementation strategy 1. New file `xenia/audit_68_host_mem_watch.h` (top-level): forward decls of helper functions in `namespace xe::audit_68`: ```cpp extern std::atomic g_active; // 0=off, 1=values, 2=addrs, 3=both void check_host_write(const void* host_ptr, uint64_t value, uint8_t size, const char* tag); void check_guest_write(uint32_t guest_va, uint64_t value, uint8_t size, const char* tag); ``` 2. New file `xenia/audit_68_host_mem_watch.cc`: lazy-parse the cvars on first call, atomic-bool sets active. Performs `Memory::active()->HostToGuestVirtual(host_ptr)` translation, then matches against value-list and addr-range list, emits XELOGI. 3. `xenia/memory.h`: add public static `Memory::active()` (returns `active_memory_`). 4. `xenia/base/memory.h`: extend `store_and_swap` specializations (uint8/uint16/uint32/uint64 only — the integer typed paths most likely to write vptrs / clear flags) to check `g_active` and call the helper. Hot path: 1 atomic load + branch when off. The added cost when on is one cmp+jne per byte/word/dword/qword store; acceptable for capture runs. 5. `xenia/memory.cc`: instrument `Memory::Zero/Fill/Copy` with calls to `check_guest_write` (each ranges through the affected guest VAs; for capture purposes we only log the first matching byte+size+tag — we don't expand to per-byte events). Total estimated LOC: ~120-160 LOC across 5 files. ## Capture protocol - Build canary with the new code. - Smoke test: `--audit_68_host_mem_watch_values=0x12345678` (no expected hits) → confirm no spurious lines, build/init OK. - Sanity test: `--audit_68_host_mem_watch_values=0x82000000` (very common vtable-base) → confirm many lines, then revert. - Run 1 (vtable install): `--audit_68_host_mem_watch_values=0x8200A208,0x8200A928 --mute=true`. Kill after ~90s. - Run 2 (voice-struct clear): `--audit_68_host_mem_watch_addrs=0x42500000-0x42600000 --mute=true`. Kill after ~30s (this is heap-region wide; likely lots of hits, capture early window only). May need narrower range once we see the first writes. - Cold-protocol: backup canary's cache (`xenia-canary/build-cross/bin/Windows/Debug/cache/`) to `/tmp/canary-cache-bak-audit-068`, wipe before run, restore after.