[Audit] Take the snapshot branch's unique probes (files only, not a merge)

auto/canary-instrumentation-snapshot-2026-07-28 was authored from a base 85
commits behind this line and re-adds the cross-build toolchain and the audit
probes as unrelated new files, so merging it conflicts in 15 places for no
gain. Everything it carries is already here except these eight, which exist
nowhere else:

  - phase_b_snapshot.{cc,h}          one-shot JSON state snapshot, fired from
                                     the JIT before the entry thread's first
                                     guest instruction (--phase_b_snapshot_dir)
  - audit_68_host_mem_watch_{base.cc,fwd.h}
  - audit_69_event_signal_watch.{cc,h}
  - audit_70_semaphore_release_watch.{cc,h}

Its xboxkrnl_xconfig.h is unrelated to the instrumentation and unreferenced
here, so it is deliberately left behind. The branch itself is kept (its
commits are not ancestors of this one).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-08-17 18:10:03 +02:00
parent 8d1e731ead
commit 55c47b17ca
8 changed files with 1994 additions and 0 deletions

View File

@@ -0,0 +1,455 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-068 host-side memory-write watch — implementation (xenia-base).
*
* Mirrors AUDIT-067 in spirit (value-CSV cvar, lazy parse, atomic-bool
* activation) but observes the HOST-side write paths instead of the JIT'd
* guest store opcodes. Captures writes performed by xe::store_and_swap<T>
* (xenia/base/memory.h) and by Memory::Zero/Fill/Copy (xenia/memory.cc).
*
* Lives in xenia-base so that the slow-path symbols resolve for callers in
* xenia-base / xenia-cpu / xenia-kernel without depending on xenia-core link
* order. The host→guest VA translation is provided by a function-pointer
* thunk that xenia::Memory::Memory() registers at construction.
*
* See xenia/base/audit_68_host_mem_watch_fwd.h for the API.
* See xenia/cpu/cpu_flags.{h,cc} for the cvars.
******************************************************************************
*/
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/threading.h"
// We need the cvars but cpu_flags.h lives in xenia-cpu. To avoid an upward
// dep we re-declare them here with the same macros — cvar.h's DECLARE_*
// macros are header-safe (just `extern` declarations) and resolve against the
// definitions in xenia-cpu/cpu_flags.cc at link time. (xenia-cpu links AFTER
// xenia-base in the executable; symbols in xenia-cpu/cpu_flags.cc are still
// resolvable from xenia-base translation units because the lld pass folds
// all libraries together at the executable level.)
DECLARE_string(audit_68_host_mem_watch_values);
DECLARE_string(audit_68_host_mem_watch_addrs);
DECLARE_string(audit_68_host_mem_read_probe);
namespace xe {
namespace audit_68 {
// Hot-path flag (declared in fwd header). Initial sentinel UINT32_MAX means
// "unparsed"; the very first slow-path call invokes ensure_parsed() which
// replaces the sentinel with the actual active bitmask (0 if both cvars are
// empty, 1/2/3 otherwise). After that, hot-path calls observe the real value
// and bail out cheaply when off.
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
// Host→guest VA translation thunk (declared in fwd header). Set by
// xenia::Memory::Memory() at construction; reset to nullptr by ~Memory().
HostToGuestThunk g_host_to_guest_thunk{nullptr};
// AUDIT-068 Session 3: guest→host translation + page-protect query thunks.
GuestToHostThunk g_guest_to_host_thunk{nullptr};
QueryProtectThunk g_query_protect_thunk{nullptr};
namespace {
constexpr size_t kMaxValues = 8;
constexpr size_t kMaxAddrRanges = 8;
struct AddrRange {
uint32_t start; // inclusive
uint32_t end; // inclusive
};
std::vector<uint32_t> g_values;
std::vector<AddrRange> g_addrs;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
try {
*out = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
return true;
} catch (...) {
return false;
}
}
void parse_values_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_values.size() < kMaxValues) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
uint32_t v;
if (parse_u32(tok, &v)) {
g_values.push_back(v);
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
void parse_addrs_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_addrs.size() < kMaxAddrRanges) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
size_t dash = tok.find('-', 2); // skip leading "0x" if present
AddrRange r{};
if (dash != std::string::npos) {
std::string s = tok.substr(0, dash);
std::string e = tok.substr(dash + 1);
trim(s);
trim(e);
uint32_t a, b;
if (parse_u32(s, &a) && parse_u32(e, &b)) {
r.start = a;
r.end = b;
g_addrs.push_back(r);
}
} else {
uint32_t a;
if (parse_u32(tok, &a)) {
r.start = a;
r.end = a + 7;
g_addrs.push_back(r);
}
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
void parse_locked() {
parse_values_csv(cvars::audit_68_host_mem_watch_values);
parse_addrs_csv(cvars::audit_68_host_mem_watch_addrs);
uint32_t bits = 0;
if (!g_values.empty()) bits |= 0x1;
if (!g_addrs.empty()) bits |= 0x2;
g_active.store(bits, std::memory_order_release);
XELOGI(
"AUDIT-068-INIT values_csv=\"{}\" addrs_csv=\"{}\" values_parsed={} "
"addr_ranges_parsed={} active=0x{:X}",
cvars::audit_68_host_mem_watch_values,
cvars::audit_68_host_mem_watch_addrs, g_values.size(), g_addrs.size(),
bits);
for (size_t i = 0; i < g_values.size(); ++i) {
XELOGI("AUDIT-068-INIT value[{}] = 0x{:08X}", i, g_values[i]);
}
for (size_t i = 0; i < g_addrs.size(); ++i) {
XELOGI("AUDIT-068-INIT addr_range[{}] = 0x{:08X}-0x{:08X}", i,
g_addrs[i].start, g_addrs[i].end);
}
}
bool value_matches(uint64_t value, uint8_t size) {
for (uint32_t v : g_values) {
if (size >= 4 && static_cast<uint32_t>(value) == v) return true;
if (size == 8 && static_cast<uint32_t>(value >> 32) == v) return true;
if (size == 2 && (v & 0xFFFF) == (value & 0xFFFF)) return true;
if (size == 1 && (v & 0xFF) == (value & 0xFF)) return true;
}
return false;
}
bool addr_matches(uint32_t guest_va, uint8_t size) {
uint32_t lo = guest_va;
uint32_t hi = guest_va + (size ? size - 1 : 0);
for (const auto& r : g_addrs) {
if (lo <= r.end && hi >= r.start) return true;
}
return false;
}
uint32_t current_tid() { return xe::threading::current_thread_id(); }
void emit(uint32_t guest_va, const void* host_ptr, uint64_t value,
uint8_t size, const char* tag) {
XELOGI(
"AUDIT-068-HOST-WRITE guest_va=0x{:08X} host_ptr=0x{:016X} "
"val=0x{:016X} sz={} fn={} host_ns={} tid={}",
guest_va, reinterpret_cast<uintptr_t>(host_ptr), value,
static_cast<uint32_t>(size), tag ? tag : "<null>",
host_ns_since_start(), current_tid());
}
// ===== AUDIT-068 Session 3 — read-mode probe state =====
constexpr size_t kMaxReadProbes = 8;
struct ReadProbe {
uint32_t guest_va;
uint8_t size; // 1, 2, 4, 8
uint64_t period_ns;
uint64_t last_value;
bool last_was_valid;
};
std::vector<ReadProbe> g_read_probes;
std::atomic<bool> g_read_probe_thread_running{false};
std::atomic<bool> g_read_probe_shutdown{false};
std::thread g_read_probe_thread;
std::once_flag g_read_probe_started;
bool parse_read_probe_tok(const std::string& tok, ReadProbe* out) {
// Expected form: "VA:SIZE:PERIOD_NS" — three colon-separated u64.
size_t c1 = tok.find(':');
if (c1 == std::string::npos) return false;
size_t c2 = tok.find(':', c1 + 1);
if (c2 == std::string::npos) return false;
std::string sva = tok.substr(0, c1);
std::string ssz = tok.substr(c1 + 1, c2 - c1 - 1);
std::string sper = tok.substr(c2 + 1);
trim(sva);
trim(ssz);
trim(sper);
try {
out->guest_va = static_cast<uint32_t>(std::stoul(sva, nullptr, 0));
uint32_t sz = static_cast<uint32_t>(std::stoul(ssz, nullptr, 0));
if (sz != 1 && sz != 2 && sz != 4 && sz != 8) return false;
out->size = static_cast<uint8_t>(sz);
out->period_ns = static_cast<uint64_t>(std::stoull(sper, nullptr, 0));
if (out->period_ns < 1000) out->period_ns = 1000; // 1us floor.
out->last_value = 0;
out->last_was_valid = false;
return true;
} catch (...) {
return false;
}
}
void parse_read_probes_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_read_probes.size() < kMaxReadProbes) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
ReadProbe rp{};
if (parse_read_probe_tok(tok, &rp)) {
g_read_probes.push_back(rp);
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
uint64_t sample_at(uint32_t guest_va, uint8_t size, bool* out_valid) {
*out_valid = false;
if (!g_guest_to_host_thunk || !g_query_protect_thunk) return 0;
uint32_t prot = 0;
if (!g_query_protect_thunk(guest_va, &prot)) return 0;
// Page must have at least read permission. The protect bits map to
// xe::memory::PageAccess: kReadOnly=1, kReadWrite=2, kExecuteReadOnly=3,
// kExecuteReadWrite=4. kNoAccess=0. Accept anything non-zero — caller
// distinguishes via the second-pass change detector anyway.
if (prot == 0) return 0;
const void* hp = g_guest_to_host_thunk(guest_va);
if (!hp) return 0;
uint64_t v = 0;
// Guest memory is big-endian. We use raw byte loads to avoid alignment
// traps for size>4 on possibly-unaligned VAs. The "value" we log is the
// host-endian interpretation of the BE bytes (matches store_and_swap's
// logging convention: the byte-swapped scalar).
const uint8_t* bp = reinterpret_cast<const uint8_t*>(hp);
switch (size) {
case 1: v = bp[0]; break;
case 2: v = (uint64_t(bp[0]) << 8) | bp[1]; break;
case 4:
v = (uint64_t(bp[0]) << 24) | (uint64_t(bp[1]) << 16) |
(uint64_t(bp[2]) << 8) | bp[3];
break;
case 8:
v = (uint64_t(bp[0]) << 56) | (uint64_t(bp[1]) << 48) |
(uint64_t(bp[2]) << 40) | (uint64_t(bp[3]) << 32) |
(uint64_t(bp[4]) << 24) | (uint64_t(bp[5]) << 16) |
(uint64_t(bp[6]) << 8) | bp[7];
break;
}
*out_valid = true;
return v;
}
void read_probe_thread_main() {
// Compute the GCD-ish min poll period across all probes; sleep that long
// between scans. Each probe fires only when its own period_ns has elapsed
// since the last sample (per-probe `next_fire_ns`).
uint64_t min_period_ns = UINT64_MAX;
for (const auto& p : g_read_probes) {
if (p.period_ns < min_period_ns) min_period_ns = p.period_ns;
}
if (min_period_ns == UINT64_MAX) return;
// Per-probe next-fire times.
std::vector<uint64_t> next_fire(g_read_probes.size(), 0);
XELOGI(
"AUDIT-068-READ-INIT probe_count={} min_period_ns={} thread spawned",
g_read_probes.size(), min_period_ns);
for (size_t i = 0; i < g_read_probes.size(); ++i) {
XELOGI("AUDIT-068-READ-INIT probe[{}] va=0x{:08X} size={} period_ns={}",
i, g_read_probes[i].guest_va,
static_cast<uint32_t>(g_read_probes[i].size),
g_read_probes[i].period_ns);
}
while (!g_read_probe_shutdown.load(std::memory_order_relaxed)) {
int64_t now_ns = host_ns_since_start();
for (size_t i = 0; i < g_read_probes.size(); ++i) {
if (static_cast<uint64_t>(now_ns) < next_fire[i]) continue;
ReadProbe& rp = g_read_probes[i];
bool valid = false;
uint64_t v = sample_at(rp.guest_va, rp.size, &valid);
if (valid) {
if (!rp.last_was_valid) {
// First successful read: emit the initial value, do NOT call it a
// "change" — but log so we know when the VA mapped.
XELOGI(
"AUDIT-068-READ-INITIAL va=0x{:08X} val=0x{:016X} sz={} "
"host_ns={} tid=probe",
rp.guest_va, v, static_cast<uint32_t>(rp.size), now_ns);
rp.last_value = v;
rp.last_was_valid = true;
} else if (v != rp.last_value) {
XELOGI(
"AUDIT-068-READ-CHANGE va=0x{:08X} old=0x{:016X} "
"new=0x{:016X} sz={} host_ns={} tid=probe",
rp.guest_va, rp.last_value, v, static_cast<uint32_t>(rp.size),
now_ns);
rp.last_value = v;
}
} else if (rp.last_was_valid) {
// Was valid, now invalid — page unmapped/reprotected.
XELOGI(
"AUDIT-068-READ-UNMAPPED va=0x{:08X} last=0x{:016X} sz={} "
"host_ns={} tid=probe",
rp.guest_va, rp.last_value, static_cast<uint32_t>(rp.size),
now_ns);
rp.last_was_valid = false;
}
next_fire[i] = static_cast<uint64_t>(now_ns) + rp.period_ns;
}
// Sleep until the next earliest fire, but no shorter than 1us and no
// longer than min_period_ns (to keep shutdown latency bounded).
uint64_t sleep_ns = min_period_ns;
if (sleep_ns < 1000) sleep_ns = 1000;
std::this_thread::sleep_for(std::chrono::nanoseconds(sleep_ns));
}
XELOGI("AUDIT-068-READ-EXIT thread shutting down");
}
void start_read_probe_thread_if_configured() {
std::call_once(g_read_probe_started, []() {
parse_read_probes_csv(cvars::audit_68_host_mem_read_probe);
if (g_read_probes.empty()) return;
if (!g_guest_to_host_thunk || !g_query_protect_thunk) {
XELOGI(
"AUDIT-068-READ-INIT thunks not ready (guest_to_host={} "
"query_protect={}) — read probe deferred",
(void*)g_guest_to_host_thunk, (void*)g_query_protect_thunk);
return;
}
g_read_probe_thread_running.store(true, std::memory_order_release);
g_read_probe_thread = std::thread(&read_probe_thread_main);
g_read_probe_thread.detach(); // best-effort; daemon-style.
});
}
} // namespace
void ensure_parsed() { std::call_once(g_parsed_flag, parse_locked); }
void check_host_write_slowpath(const void* host_ptr, uint64_t value,
uint8_t size, const char* tag) {
// AUDIT-068 Session 2: defer parsing until Memory::Memory() has registered
// the host→guest thunk. This guarantees the cmdline cvar override has been
// applied AND the logging subsystem is alive before we latch g_active.
// Without this gate, a be<T>::set() call during static-init (e.g. from a
// global initializer in another translation unit) would trigger
// parse_locked() before cpu_flags.cc's cvar objects are constructed —
// latching g_active=0 permanently and silencing the watch.
HostToGuestThunk thunk = g_host_to_guest_thunk;
if (!thunk) return;
ensure_parsed();
// AUDIT-068 Session 3: lazy-start the read-probe poll thread. Same gate as
// ensure_parsed() — must come after Memory::Memory() has registered the
// thunks so the probe can read pages safely.
start_read_probe_thread_if_configured();
uint32_t active = g_active.load(std::memory_order_acquire);
if (active == 0) return;
uint32_t guest_va = 0;
if (thunk) {
guest_va = thunk(host_ptr);
}
bool hit = false;
if ((active & 0x1) && value_matches(value, size)) hit = true;
if (!hit && (active & 0x2) && thunk && addr_matches(guest_va, size)) {
hit = true;
}
if (!hit) return;
emit(guest_va, host_ptr, value, size, tag);
}
void check_guest_va_slowpath(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag) {
// AUDIT-068 Session 2: same static-init gate as check_host_write_slowpath.
// Callers (Memory::Zero/Fill/Copy + xex_module audit68_prescan_memcpy) only
// run after Memory::Memory(), but defensive in case of future expansion.
if (!g_host_to_guest_thunk) return;
ensure_parsed();
uint32_t active = g_active.load(std::memory_order_acquire);
if (active == 0) return;
bool hit = false;
if ((active & 0x1) && value_matches(value, size)) hit = true;
if (!hit && (active & 0x2) && addr_matches(guest_va, size)) hit = true;
if (!hit) return;
emit(guest_va, nullptr, value, size, tag);
}
} // namespace audit_68
} // namespace xe

View File

@@ -0,0 +1,95 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-068: host-side memory-write watch — forward declarations only.
*
* Declarations here are intentionally minimal so that xenia/base/memory.h can
* include this without pulling in xenia/memory.h (which would create a
* circular dependency: xenia-base → xenia-core → xenia-base). The full
* definitions live in xenia/audit_68_host_mem_watch.{h,cc} (xenia-core).
*
* Hot path: callers (the integer specializations of xe::store_and_swap<T>)
* load the atomic flag once. When it is 0 (default), no further work is done
* — a single relaxed atomic load and a predictable branch.
******************************************************************************
*/
#ifndef XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_
#define XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace audit_68 {
// 0 = inactive (default). Non-zero = the cvars have been parsed and at least
// one watch is configured. Set lazily by check_host_write_slowpath() on first
// call after cvar parsing. Loaded relaxed on the hot path.
//
// Implementation lives in xenia-base (audit_68_host_mem_watch_base.cc) so
// that callers in xenia-base/xenia-cpu/xenia-kernel can resolve the symbol
// without depending on xenia-core link order.
extern std::atomic<uint32_t> g_active;
// Host-pointer → guest-VA translation thunk. xenia/memory.cc::Memory::Memory()
// registers a function pointer here that wraps Memory::HostToGuestVirtual.
// Until set, the slow path falls back to logging the raw host pointer.
using HostToGuestThunk = uint32_t (*)(const void*);
extern HostToGuestThunk g_host_to_guest_thunk;
// AUDIT-068 Session 3 — read-mode probe support.
//
// Guest-VA → host-pointer translation thunk (wraps Memory::TranslateVirtual).
// Used by the read-probe poll thread to sample bytes at configured guest VAs.
// May return non-null even for unmapped/uncommitted VAs (the underlying
// translation is arithmetic — virtual_membase_ + va) — callers MUST consult
// the QueryProtect thunk before dereferencing.
using GuestToHostThunk = const void* (*)(uint32_t);
extern GuestToHostThunk g_guest_to_host_thunk;
// Returns true iff the page containing `guest_va` is committed and readable;
// out_protect receives the raw page protect bits (kProtectRead, etc.). Wraps
// Memory::LookupHeap() + BaseHeap::QueryProtect(). Used as a guard before the
// read-probe samples bytes (early-boot heap-not-yet-mapped path must NOT
// crash).
using QueryProtectThunk = bool (*)(uint32_t, uint32_t* /*out_protect*/);
extern QueryProtectThunk g_query_protect_thunk;
// Slow path. Only invoked when g_active is non-zero. Implementation in
// xenia/base/audit_68_host_mem_watch_base.cc (xenia-base).
//
// host_ptr: the host pointer being written (from store_and_swap's `mem`).
// value: the value being stored (zero-extended to u64).
// size: 1, 2, 4 or 8.
// tag: caller-provided tag string (e.g. "store_and_swap<u32>"). Logged
// verbatim, no formatting. Must be a static string (lifetime
// beyond this call).
void check_host_write_slowpath(const void* host_ptr, uint64_t value,
uint8_t size, const char* tag);
// Same as above, but with a known guest VA (for callers like Memory::Zero/
// Fill/Copy that have the VA but not a single host pointer).
void check_guest_va_slowpath(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag);
// Inline hot-path wrappers. Single relaxed atomic load + branch when inactive.
inline void check_host_write(const void* host_ptr, uint64_t value, uint8_t size,
const char* tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_host_write_slowpath(host_ptr, value, size, tag);
}
}
inline void check_guest_va(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_guest_va_slowpath(guest_va, value, size, tag);
}
}
} // namespace audit_68
} // namespace xe
#endif // XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_

View File

@@ -0,0 +1,193 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-069 event-signal watch — implementation (xenia-kernel).
*
* Mirrors AUDIT-068's lazy-parse + UINT32_MAX sentinel pattern. The hot path
* (inline in the header) reads g_active relaxed and bails out cheaply when
* inactive; the slowpath here parses the cvars on first fire and then logs
* matches.
******************************************************************************
*/
#include "xenia/kernel/audit_69_event_signal_watch.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/xthread.h"
DECLARE_string(audit_69_event_signal_watch);
DECLARE_string(audit_69_event_signal_native_ptr);
DECLARE_bool(audit_69_log_all_sets);
namespace xe {
namespace kernel {
namespace audit_69 {
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
namespace {
constexpr size_t kMaxEntries = 4;
std::vector<uint32_t> g_handles;
std::vector<uint32_t> g_native_ptrs;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
if (tok.empty()) return false;
const char* p = tok.c_str();
int base = 10;
if (tok.size() >= 2 && (tok[0] == '0') && (tok[1] == 'x' || tok[1] == 'X')) {
p += 2;
base = 16;
}
if (*p == 0) return false;
uint64_t v = 0;
while (*p) {
int d;
if (*p >= '0' && *p <= '9')
d = *p - '0';
else if (base == 16 && *p >= 'a' && *p <= 'f')
d = 10 + (*p - 'a');
else if (base == 16 && *p >= 'A' && *p <= 'F')
d = 10 + (*p - 'A');
else
return false;
v = v * base + d;
if (v > 0xFFFFFFFFu) return false;
++p;
}
*out = static_cast<uint32_t>(v);
return true;
}
void parse_csv_to_u32_vec(const std::string& csv, std::vector<uint32_t>* out) {
out->clear();
std::string cur;
auto flush = [&]() {
trim(cur);
if (!cur.empty()) {
uint32_t v = 0;
if (parse_u32(cur, &v)) {
if (out->size() < kMaxEntries) {
out->push_back(v);
}
}
}
cur.clear();
};
for (char c : csv) {
if (c == ',') {
flush();
} else {
cur.push_back(c);
}
}
flush();
}
void parse_locked() {
parse_csv_to_u32_vec(cvars::audit_69_event_signal_watch, &g_handles);
parse_csv_to_u32_vec(cvars::audit_69_event_signal_native_ptr,
&g_native_ptrs);
uint32_t active = 0;
if (!g_handles.empty()) active |= 1u;
if (!g_native_ptrs.empty()) active |= 2u;
if (cvars::audit_69_log_all_sets) active |= 4u;
g_active.store(active, std::memory_order_relaxed);
if (active != 0) {
XELOGI(
"AUDIT-069-INIT handles_n={} native_ptrs_n={} (active=0x{:X}, "
"host_ns={})",
g_handles.size(), g_native_ptrs.size(), active,
host_ns_since_start());
for (size_t i = 0; i < g_handles.size(); ++i) {
XELOGI("AUDIT-069-INIT-HANDLE[{}]=0x{:08X}", i, g_handles[i]);
}
for (size_t i = 0; i < g_native_ptrs.size(); ++i) {
XELOGI("AUDIT-069-INIT-NATIVE[{}]=0x{:08X}", i, g_native_ptrs[i]);
}
}
}
bool matches(uint32_t host_handle, uint32_t native_ptr) {
if (cvars::audit_69_log_all_sets) {
return true;
}
if (host_handle != 0 &&
std::find(g_handles.begin(), g_handles.end(), host_handle) !=
g_handles.end()) {
return true;
}
if (native_ptr != 0 &&
std::find(g_native_ptrs.begin(), g_native_ptrs.end(), native_ptr) !=
g_native_ptrs.end()) {
return true;
}
return false;
}
} // namespace
void check_event_set_slowpath(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag) {
std::call_once(g_parsed_flag, parse_locked);
uint32_t active = g_active.load(std::memory_order_relaxed);
if (active == 0) return;
if (!matches(host_handle, native_ptr)) return;
// Capture caller LR + tid via the current XThread (best-effort).
// Note: PPCContext has no PC field — only LR is meaningful here. The
// log emits `pc=lr` for grep-compatibility with AUDIT-061 lines, but the
// actual caller PC == lr-of-current-call, i.e. the return address of
// whichever kernel export wrapper invoked the setter.
uint32_t lr = 0;
uint32_t tid = 0;
if (auto* thr = XThread::TryGetCurrentThread()) {
auto* ctx = thr->thread_state() ? thr->thread_state()->context() : nullptr;
if (ctx) {
lr = static_cast<uint32_t>(ctx->lr);
}
tid = thr->thread_id();
}
XELOGI(
"AUDIT-069-SIGNAL fn={} target_handle=0x{:08X} native_ptr=0x{:08X} "
"lr=0x{:08X} tid={} host_ns={}",
fn_tag, host_handle, native_ptr, lr, tid, host_ns_since_start());
}
} // namespace audit_69
} // namespace kernel
} // namespace xe

View File

@@ -0,0 +1,51 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-069: event-signal watch — guest-handle/native-pointer matched logger
* for XEvent::Set / Pulse and the Ke/Nt event setter entry points.
*
* Mirrors the AUDIT-068 hot-path discipline:
* - UINT32_MAX sentinel in g_active means "cvars not yet parsed".
* - First slowpath call drives a std::once parse of the configured cvars;
* g_active is replaced with the active-bitmask (0 if both empty).
* - Hot path = single relaxed atomic load + predictable branch.
*
* Slowpath is implemented in audit_69_event_signal_watch.cc.
******************************************************************************
*/
#ifndef XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_
#define XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace kernel {
namespace audit_69 {
// 0 = inactive (default observed after first parse). Non-zero = at least one
// watch is configured. UINT32_MAX = sentinel for "not yet parsed".
extern std::atomic<uint32_t> g_active;
// Slowpath. host_handle = caller-known guest event handle (0 if unknown);
// native_ptr = guest VA of the X_KEVENT (0 if unknown); fn_tag = static
// string literal (e.g. "XEvent::Set"). LR is captured from the JIT-saved
// link register via xe::kernel::XThread::GetCurrentThread().
void check_event_set_slowpath(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag);
// Hot-path wrapper.
inline void check_event_set(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_event_set_slowpath(host_handle, native_ptr, fn_tag);
}
}
} // namespace audit_69
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_

View File

@@ -0,0 +1,183 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-070 semaphore-release watch — implementation (xenia-kernel).
*
* Mirrors AUDIT-069's lazy-parse + UINT32_MAX sentinel pattern. The hot path
* (inline in the header) reads g_active relaxed and bails out cheaply when
* inactive; the slowpath here parses the cvars on first fire and then logs
* matches.
******************************************************************************
*/
#include "xenia/kernel/audit_70_semaphore_release_watch.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/xthread.h"
DECLARE_string(audit_70_semaphore_release_watch);
DECLARE_bool(audit_70_log_all_releases);
namespace xe {
namespace kernel {
namespace audit_70 {
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
namespace {
constexpr size_t kMaxEntries = 4;
std::vector<uint32_t> g_handles;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
if (tok.empty()) return false;
const char* p = tok.c_str();
int base = 10;
if (tok.size() >= 2 && (tok[0] == '0') && (tok[1] == 'x' || tok[1] == 'X')) {
p += 2;
base = 16;
}
if (*p == 0) return false;
uint64_t v = 0;
while (*p) {
int d;
if (*p >= '0' && *p <= '9')
d = *p - '0';
else if (base == 16 && *p >= 'a' && *p <= 'f')
d = 10 + (*p - 'a');
else if (base == 16 && *p >= 'A' && *p <= 'F')
d = 10 + (*p - 'A');
else
return false;
v = v * base + d;
if (v > 0xFFFFFFFFu) return false;
++p;
}
*out = static_cast<uint32_t>(v);
return true;
}
void parse_csv_to_u32_vec(const std::string& csv, std::vector<uint32_t>* out) {
out->clear();
std::string cur;
auto flush = [&]() {
trim(cur);
if (!cur.empty()) {
uint32_t v = 0;
if (parse_u32(cur, &v)) {
if (out->size() < kMaxEntries) {
out->push_back(v);
}
}
}
cur.clear();
};
for (char c : csv) {
if (c == ',') {
flush();
} else {
cur.push_back(c);
}
}
flush();
}
void parse_locked() {
parse_csv_to_u32_vec(cvars::audit_70_semaphore_release_watch, &g_handles);
uint32_t active = 0;
if (!g_handles.empty()) active |= 1u;
if (cvars::audit_70_log_all_releases) active |= 2u;
g_active.store(active, std::memory_order_relaxed);
if (active != 0) {
XELOGI(
"AUDIT-070-INIT handles_n={} log_all={} (active=0x{:X}, host_ns={})",
g_handles.size(), cvars::audit_70_log_all_releases ? 1 : 0, active,
host_ns_since_start());
for (size_t i = 0; i < g_handles.size(); ++i) {
XELOGI("AUDIT-070-INIT-HANDLE[{}]=0x{:08X}", i, g_handles[i]);
}
}
}
bool matches(uint32_t host_handle) {
if (cvars::audit_70_log_all_releases) {
return true;
}
if (host_handle != 0 &&
std::find(g_handles.begin(), g_handles.end(), host_handle) !=
g_handles.end()) {
return true;
}
return false;
}
} // namespace
void check_release_slowpath(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count) {
std::call_once(g_parsed_flag, parse_locked);
uint32_t active = g_active.load(std::memory_order_relaxed);
if (active == 0) return;
if (!matches(host_handle)) return;
// Capture caller LR + tid via the current XThread (best-effort).
uint32_t lr = 0;
uint32_t tid = 0;
if (auto* thr = XThread::TryGetCurrentThread()) {
auto* ctx = thr->thread_state() ? thr->thread_state()->context() : nullptr;
if (ctx) {
lr = static_cast<uint32_t>(ctx->lr);
}
tid = thr->thread_id();
}
// new_count = previous + release_count (capped at INT32_MAX). Both engines
// log it pre-emptively for direct comparison; if release would exceed limit
// the slowpath caller's caller logs the limit-exceeded path separately
// (see NtReleaseSemaphore_entry's success-check).
int64_t new_count =
static_cast<int64_t>(previous_count) + static_cast<int64_t>(release_count);
XELOGI(
"AUDIT-070-RELEASE fn={} handle=0x{:08X} count={} prev_count={} "
"new_count={} lr=0x{:08X} tid={} host_ns={}",
fn_tag, host_handle, release_count, previous_count, new_count, lr, tid,
host_ns_since_start());
}
} // namespace audit_70
} // namespace kernel
} // namespace xe

View File

@@ -0,0 +1,52 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch — guest-handle
* matched logger for NtReleaseSemaphore_entry and xeKeReleaseSemaphore.
*
* Mirrors the AUDIT-068/069 hot-path discipline:
* - UINT32_MAX sentinel in g_active means "cvars not yet parsed".
* - First slowpath call drives a std::once parse of the configured cvars;
* g_active is replaced with the active-bitmask (0 if both empty).
* - Hot path = single relaxed atomic load + predictable branch.
*
* Slowpath is implemented in audit_70_semaphore_release_watch.cc.
******************************************************************************
*/
#ifndef XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_
#define XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace kernel {
namespace audit_70 {
// 0 = inactive (default observed after first parse). Non-zero = at least one
// watch is configured. UINT32_MAX = sentinel for "not yet parsed".
extern std::atomic<uint32_t> g_active;
// Slowpath. host_handle = guest semaphore handle (0 if unknown);
// fn_tag = static string literal (e.g. "NtReleaseSemaphore");
// release_count = the adjustment requested;
// previous_count = the value returned by the underlying Release().
// LR + tid are captured from the current XThread.
void check_release_slowpath(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count);
// Hot-path wrapper.
inline void check_release(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_release_slowpath(host_handle, fn_tag, release_count, previous_count);
}
}
} // namespace audit_70
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_

View File

@@ -0,0 +1,922 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Phase B initial-state snapshot. See phase_b_snapshot.h.
******************************************************************************
*/
#include "xenia/kernel/phase_b_snapshot.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
#include "third_party/crypto/sha256.h"
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/cvar.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/util/object_table.h"
#include "xenia/kernel/xobject.h"
#include "xenia/kernel/xthread.h"
#include "xenia/memory.h"
#include "xenia/vfs/device.h"
#include "xenia/vfs/entry.h"
#include "xenia/vfs/virtual_file_system.h"
namespace xe {
namespace kernel {
namespace phase_b {
namespace {
constexpr uint32_t kSchemaVersion = 1;
constexpr const char* kEngineName = "canary";
// One-shot guard. CAS-claim to ensure only the entry thread fires the
// snapshot; release on guard-fail so a non-entry thread reaching its
// first instruction first does not steal the shot.
std::atomic<bool> g_claimed{false};
std::atomic<bool> g_done{false};
// ---------- string helpers ----------
std::string JsonEscape(const std::string& s) {
std::string out;
out.reserve(s.size() + 2);
for (unsigned char c : s) {
if (c == '\\' || c == '"') {
out.push_back('\\');
out.push_back(static_cast<char>(c));
} else if (c == '\n') {
out += "\\n";
} else if (c == '\r') {
out += "\\r";
} else if (c == '\t') {
out += "\\t";
} else if (c < 0x20) {
out += fmt::format("\\u{:04x}", c);
} else {
out.push_back(static_cast<char>(c));
}
}
return out;
}
std::string Hex32(uint32_t v) { return fmt::format("\"0x{:08x}\"", v); }
std::string Hex64(uint64_t v) { return fmt::format("\"0x{:016x}\"", v); }
std::string Sha256Hex(const uint8_t* data, size_t len) {
::sha256::SHA256 h;
h.add(data, len);
return h.getHash();
}
// Stream-style writer that produces newline-indented JSON with sorted keys.
// We build a small tree first then serialize, so ordering is deterministic
// independent of any std::unordered_map iteration order.
class JsonNode {
public:
enum class Kind { Null, Bool, Int, UInt, IntStr, Str, Array, Object, Raw };
JsonNode() : kind_(Kind::Null) {}
static JsonNode Null() { JsonNode n; n.kind_ = Kind::Null; return n; }
static JsonNode Boolean(bool b) {
JsonNode n;
n.kind_ = Kind::Bool;
n.bool_ = b;
return n;
}
static JsonNode Integer(int64_t i) {
JsonNode n;
n.kind_ = Kind::Int;
n.int_ = i;
return n;
}
static JsonNode Unsigned(uint64_t u) {
JsonNode n;
n.kind_ = Kind::UInt;
n.uint_ = u;
return n;
}
// Pre-formatted JSON literal (e.g. `"0x..."`, raw object/array source).
static JsonNode Raw(std::string s) {
JsonNode n;
n.kind_ = Kind::Raw;
n.str_ = std::move(s);
return n;
}
static JsonNode String(std::string s) {
JsonNode n;
n.kind_ = Kind::Str;
n.str_ = std::move(s);
return n;
}
static JsonNode Array(std::vector<JsonNode> v) {
JsonNode n;
n.kind_ = Kind::Array;
n.array_ = std::move(v);
return n;
}
static JsonNode Object() {
JsonNode n;
n.kind_ = Kind::Object;
return n;
}
// Object that preserves insertion order (used at the top level of files,
// where the user-facing key ordering is canonical).
static JsonNode OrderedObject() {
JsonNode n;
n.kind_ = Kind::Object;
n.ordered_ = true;
return n;
}
void Set(const std::string& key, JsonNode v) {
obj_[key] = std::move(v);
if (ordered_) ordered_keys_.push_back(key);
}
void Serialize(std::string& out, int indent = 0) const {
auto pad = [&](int n) {
out.append(static_cast<size_t>(n * 2), ' ');
};
switch (kind_) {
case Kind::Null:
out += "null";
break;
case Kind::Bool:
out += bool_ ? "true" : "false";
break;
case Kind::Int:
out += std::to_string(int_);
break;
case Kind::UInt:
out += std::to_string(uint_);
break;
case Kind::Raw:
out += str_;
break;
case Kind::Str:
out.push_back('"');
out += JsonEscape(str_);
out.push_back('"');
break;
case Kind::Array: {
if (array_.empty()) {
out += "[]";
break;
}
out += "[\n";
for (size_t i = 0; i < array_.size(); ++i) {
pad(indent + 1);
array_[i].Serialize(out, indent + 1);
if (i + 1 < array_.size()) out += ",";
out += "\n";
}
pad(indent);
out += "]";
break;
}
case Kind::Object: {
if (obj_.empty()) {
out += "{}";
break;
}
out += "{\n";
std::vector<std::string> keys;
if (ordered_) {
keys = ordered_keys_;
} else {
keys.reserve(obj_.size());
for (const auto& [k, _] : obj_) keys.push_back(k);
std::sort(keys.begin(), keys.end());
}
for (size_t i = 0; i < keys.size(); ++i) {
pad(indent + 1);
out.push_back('"');
out += JsonEscape(keys[i]);
out += "\": ";
obj_.at(keys[i]).Serialize(out, indent + 1);
if (i + 1 < keys.size()) out += ",";
out += "\n";
}
pad(indent);
out += "}";
break;
}
}
}
private:
Kind kind_;
bool bool_ = false;
int64_t int_ = 0;
uint64_t uint_ = 0;
std::string str_;
std::vector<JsonNode> array_;
std::map<std::string, JsonNode> obj_;
bool ordered_ = false;
std::vector<std::string> ordered_keys_;
};
// Sync-then-fclose helper. Returns SHA-256 of the file's bytes.
std::string WriteFileAndHash(const std::filesystem::path& path,
const std::string& content) {
std::FILE* f = std::fopen(path.string().c_str(), "wb");
if (!f) {
return std::string(64, '0');
}
std::fwrite(content.data(), 1, content.size(), f);
std::fflush(f);
#if defined(_MSC_VER)
// Best effort on Windows — _commit takes a file descriptor.
// fmt:omit on cross-build to avoid Win32-only headers in this TU.
#else
// Unix-style fsync would go here; skipped to keep deps minimal in this TU.
#endif
std::fclose(f);
return Sha256Hex(reinterpret_cast<const uint8_t*>(content.data()),
content.size());
}
// ---------- cpu_state.json ----------
JsonNode BuildCpuState(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_pc) {
auto* ctx = thread_state->context();
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
// Canary's PPCContext doesn't track PC explicitly — the JIT dispatch
// loop owns it. At the snapshot point, the about-to-execute PC equals
// the `entry_pc` arg passed to FireIfEntryThread.
root.Set("pc", JsonNode::Raw(Hex32(entry_pc)));
root.Set("lr", JsonNode::Raw(Hex64(ctx->lr)));
root.Set("ctr", JsonNode::Raw(Hex64(ctx->ctr)));
root.Set("msr", JsonNode::Raw(Hex64(ctx->msr)));
root.Set("vrsave", JsonNode::Raw(Hex32(ctx->vrsave)));
root.Set("fpscr", JsonNode::Raw(Hex32(ctx->fpscr.value)));
auto xer = JsonNode::Object();
xer.Set("ca", JsonNode::Unsigned(ctx->xer_ca));
xer.Set("ov", JsonNode::Unsigned(ctx->xer_ov));
xer.Set("so", JsonNode::Unsigned(ctx->xer_so));
// tbc is not modelled per-field in canary's PPCContext; emit 0.
xer.Set("tbc", JsonNode::Unsigned(0));
root.Set("xer", std::move(xer));
// CR as 8 nibbles 0xN. Diff tool compares array positionally.
std::vector<JsonNode> cr_arr;
cr_arr.reserve(8);
uint64_t cr = ctx->cr();
for (int i = 0; i < 8; ++i) {
uint32_t nibble = (cr >> (28 - i * 4)) & 0xF;
cr_arr.push_back(JsonNode::Raw(fmt::format("\"0x{:x}\"", nibble)));
}
root.Set("cr", JsonNode::Array(std::move(cr_arr)));
std::vector<JsonNode> gpr;
gpr.reserve(32);
for (int i = 0; i < 32; ++i) {
gpr.push_back(JsonNode::Raw(Hex64(ctx->r[i])));
}
root.Set("gpr", JsonNode::Array(std::move(gpr)));
std::vector<JsonNode> fpr;
fpr.reserve(32);
for (int i = 0; i < 32; ++i) {
uint64_t bits = 0;
std::memcpy(&bits, &ctx->f[i], sizeof(bits));
fpr.push_back(JsonNode::Raw(Hex64(bits)));
}
root.Set("fpr", JsonNode::Array(std::move(fpr)));
// Emit 32 hex chars of the raw 16 bytes (byte 0 first). Ours uses
// big-endian-stored bytes; canary's union exposes u8[16] in the same
// host order. Emitting bytes[0]..bytes[15] keeps both engines' VR
// serializations directly comparable.
std::vector<JsonNode> vr;
vr.reserve(128);
for (int i = 0; i < 128; ++i) {
std::string s;
s.reserve(32);
for (int j = 0; j < 16; ++j) {
s += fmt::format("{:02x}", ctx->v[i].u8[j]);
}
vr.push_back(JsonNode::String(std::move(s)));
}
root.Set("vr", JsonNode::Array(std::move(vr)));
std::string vscr_s;
vscr_s.reserve(32);
for (int j = 0; j < 16; ++j) {
vscr_s += fmt::format("{:02x}", ctx->vscr_vec.u8[j]);
}
root.Set("vscr", JsonNode::String(std::move(vscr_s)));
root.Set("thread_id", JsonNode::Unsigned(xthread ? xthread->thread_id() : 0));
root.Set("hw_id", JsonNode::Unsigned(0));
root.Set("stack_base",
JsonNode::Raw(Hex32(xthread ? xthread->stack_base() : 0)));
root.Set("stack_limit",
JsonNode::Raw(Hex32(xthread ? xthread->stack_limit() : 0)));
root.Set("tls_base",
JsonNode::Raw(Hex32(xthread ? xthread->tls_ptr() : 0)));
root.Set("pcr_base",
JsonNode::Raw(Hex32(xthread ? xthread->pcr_ptr() : 0)));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("hw_id"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- memory.json ----------
struct CommittedRegion {
uint32_t start;
uint32_t end;
uint32_t protect;
std::string sha256;
};
void WalkHeapRegions(Memory* memory, uint32_t heap_base_addr,
std::vector<CommittedRegion>& out_regions,
std::map<std::string, uint64_t>& out_hist) {
auto* heap = memory->LookupHeap(heap_base_addr);
if (!heap) return;
const uint32_t heap_base = heap->heap_base();
const uint32_t heap_size = heap->heap_size();
const uint32_t page_size = heap->page_size();
// Read bytes via `virtual_membase + guest_address`. This is sound for
// the four guest-virtual heaps (0x00/0x40/0x80/0x90); physical heaps
// (0xA0/0xC0/0xE0) mirror physical_membase and can include host pages
// that are reserved but not backed at boot — reading them faults.
// Phase B only walks virtual heaps; the caller filters which bases
// to probe.
uint8_t* membase = memory->virtual_membase();
uint32_t cursor = heap_base;
uint32_t end = heap_base + heap_size;
while (cursor < end) {
HeapAllocationInfo info;
if (!heap->QueryRegionInfo(cursor, &info)) break;
if (info.region_size == 0) {
cursor += page_size;
continue;
}
if (info.state == 0) {
out_hist["free"] += info.region_size / page_size;
} else if ((info.state & 0x2) != 0) { // kMemoryAllocationCommit
out_hist["committed"] += info.region_size / page_size;
// Hash region contents from virtual_membase + cursor.
std::string h = membase ? Sha256Hex(membase + cursor, info.region_size)
: std::string(64, '0');
CommittedRegion r;
r.start = cursor;
r.end = cursor + info.region_size;
r.protect = info.protect;
r.sha256 = h;
out_regions.push_back(r);
} else {
out_hist["reserved"] += info.region_size / page_size;
}
cursor += info.region_size;
}
}
JsonNode BuildMemory(KernelState* kstate, bool dump_section_content) {
Memory* memory = kstate->memory();
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
root.Set("page_size", JsonNode::Unsigned(4096));
root.Set("guest_address_space_bytes",
JsonNode::Unsigned(uint64_t{0x100000000}));
// Phase B walks a FIXED set of named regions whose host backing is
// guaranteed live at entry_point time: the XEX image, the entry
// thread's stack, its PCR, its TLS block. A blanket "walk every
// committed page across all heaps" approach is unsafe because
// canary's `QueryRegionInfo` reports `state=COMMIT` for pages whose
// host mapping may still be lazy (Windows reserved-but-not-committed,
// physical heap mirrors with unmapped backing). Reading those host
// VAs faults — see Wine page-fault during initial bring-up.
//
// Named regions are sufficient for Phase B's purpose (catalog
// divergences at the snapshot point); the diff tool compares the
// ordered list, so any region present in one engine and absent in
// the other is a σ-structural divergence.
uint8_t* membase = memory->virtual_membase();
std::vector<CommittedRegion> all_regions;
std::map<std::string, uint64_t> global_hist;
auto hash_named_region = [&](uint32_t start, uint32_t size) {
if (size == 0 || !membase) return;
std::string h = Sha256Hex(membase + start, size);
CommittedRegion r;
r.start = start;
r.end = start + size;
r.protect = 0;
r.sha256 = h;
all_regions.push_back(r);
global_hist["committed"] += size / 4096;
};
// 1. XEX image.
if (auto exec_module = kstate->GetExecutableModule()) {
uint32_t image_base = exec_module->xex_module()->base_address();
uint32_t image_size = exec_module->xex_module()->image_size();
if (image_base && image_size) {
hash_named_region(image_base, image_size);
}
}
// 2. Entry thread's stack + PCR + TLS — accessed via the XThread
// that's about to execute (resolved from the snapshot helper's
// arguments by passing a small accessor).
if (auto* xthread = XThread::GetCurrentThread()) {
uint32_t stack_base = xthread->stack_base();
uint32_t stack_limit = xthread->stack_limit();
if (stack_base > stack_limit) {
hash_named_region(stack_limit, stack_base - stack_limit);
}
uint32_t pcr = xthread->pcr_ptr();
if (pcr) {
hash_named_region(pcr, 0x1000);
}
uint32_t tls = xthread->tls_ptr();
if (tls) {
hash_named_region(tls, 0x1000);
}
}
// Heap descriptors — emit the four virtual heaps' bounds. Histograms
// come from QueryRegionInfo (which is safe to call — it doesn't read
// backing pages).
const uint32_t heap_probes[] = {
0x00000000u, 0x40000000u, 0x80000000u, 0x90000000u,
};
std::vector<JsonNode> heaps_arr;
for (uint32_t base : heap_probes) {
auto* heap = memory->LookupHeap(base);
if (!heap) continue;
std::map<std::string, uint64_t> hist;
uint32_t cursor = heap->heap_base();
uint32_t hend = heap->heap_base() + heap->heap_size();
while (cursor < hend) {
HeapAllocationInfo info;
if (!heap->QueryRegionInfo(cursor, &info)) break;
if (info.region_size == 0) {
cursor += heap->page_size();
continue;
}
if (info.state == 0) {
hist["free"] += info.region_size / heap->page_size();
} else if ((info.state & 0x2) != 0) {
hist["committed"] += info.region_size / heap->page_size();
} else {
hist["reserved"] += info.region_size / heap->page_size();
}
cursor += info.region_size;
}
for (const auto& [k, v] : hist) global_hist[k] += v;
auto heap_obj = JsonNode::Object();
heap_obj.Set("name", JsonNode::String(fmt::format("v{:08x}", base)));
heap_obj.Set("base", JsonNode::Raw(Hex32(heap->heap_base())));
heap_obj.Set("size", JsonNode::Raw(Hex32(heap->heap_size())));
heap_obj.Set("page_size", JsonNode::Unsigned(heap->page_size()));
auto hist_obj = JsonNode::Object();
for (const auto& [k, v] : hist) {
hist_obj.Set(k, JsonNode::Unsigned(v));
}
heap_obj.Set("page_state_histogram", std::move(hist_obj));
heaps_arr.push_back(std::move(heap_obj));
}
root.Set("heaps", JsonNode::Array(std::move(heaps_arr)));
// Sort regions by (start, end).
std::sort(all_regions.begin(), all_regions.end(),
[](const CommittedRegion& a, const CommittedRegion& b) {
if (a.start != b.start) return a.start < b.start;
return a.end < b.end;
});
uint64_t committed_pages = 0;
std::vector<JsonNode> regions_arr;
regions_arr.reserve(all_regions.size());
for (const auto& r : all_regions) {
auto ro = JsonNode::Object();
ro.Set("start", JsonNode::Raw(Hex32(r.start)));
ro.Set("end", JsonNode::Raw(Hex32(r.end)));
ro.Set("byte_count", JsonNode::Unsigned(r.end - r.start));
ro.Set("protect", JsonNode::Unsigned(r.protect));
ro.Set("sha256", JsonNode::String(r.sha256));
ro.Set("section_kind", JsonNode::Null());
regions_arr.push_back(std::move(ro));
committed_pages += (r.end - r.start) / 4096;
}
root.Set("regions", JsonNode::Array(std::move(regions_arr)));
root.Set("committed_pages_total", JsonNode::Unsigned(committed_pages));
if (dump_section_content) {
std::vector<JsonNode> sec;
for (const auto& r : all_regions) {
auto so = JsonNode::Object();
so.Set("start", JsonNode::Raw(Hex32(r.start)));
so.Set("end", JsonNode::Raw(Hex32(r.end)));
so.Set("sha256", JsonNode::String(r.sha256));
so.Set("content_b64", JsonNode::String("")); // Stubbed.
sec.push_back(std::move(so));
}
root.Set("section_contents", JsonNode::Array(std::move(sec)));
} else {
root.Set("section_contents", JsonNode::Null());
}
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_base_pointer"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- kernel.json ----------
const char* TypeName(XObject::Type t) {
switch (t) {
case XObject::Type::Event: return "Event";
case XObject::Type::Mutant: return "Mutant";
case XObject::Type::Semaphore: return "Semaphore";
case XObject::Type::Thread: return "Thread";
case XObject::Type::Timer: return "Timer";
case XObject::Type::File: return "File";
case XObject::Type::IOCompletion: return "IOCompletion";
case XObject::Type::Module: return "Module";
case XObject::Type::Enumerator: return "Enumerator";
case XObject::Type::NotifyListener: return "NotifyListener";
case XObject::Type::Session: return "Session";
case XObject::Type::Socket: return "Socket";
case XObject::Type::SymbolicLink: return "SymbolicLink";
case XObject::Type::Device: return "Device";
case XObject::Type::Undefined: return "Undefined";
}
return "Undefined";
}
uint32_t TypeCode(XObject::Type t) {
switch (t) {
case XObject::Type::Event: return 0x01;
case XObject::Type::Mutant: return 0x02;
case XObject::Type::Semaphore: return 0x03;
case XObject::Type::Timer: return 0x04;
case XObject::Type::Thread: return 0x05;
case XObject::Type::File: return 0x06;
case XObject::Type::IOCompletion: return 0x07;
case XObject::Type::Module: return 0x08;
case XObject::Type::Enumerator: return 0x09;
case XObject::Type::NotifyListener: return 0x0B;
default: return 0x00;
}
}
// FNV-1a 64-bit semantic-id, matching event_log.cc::ComputeSemanticId.
// At snapshot time we don't have a meaningful create_site_pc/create_tid/
// create_idx tuple for every object (they were minted before Phase B
// instrumentation existed), so fall back to a stable identity hash over
// (object_type, primary_handle). This is consistent across runs of the
// same engine; diff tool compares semantic IDs across engines only when
// both sides also stamp the same identity inputs. For Phase B's purposes
// (initial-state snapshot), the object population is tiny (≤ 2 entries
// at entry-point time: the main thread, plus an executable module ref),
// so a simple stable hash suffices.
uint64_t StableObjectId(uint32_t type_code, uint32_t raw_handle) {
uint8_t bytes[8];
for (int i = 0; i < 4; ++i) bytes[i] = (type_code >> (i * 8)) & 0xFF;
for (int i = 0; i < 4; ++i) bytes[4 + i] = (raw_handle >> (i * 8)) & 0xFF;
uint64_t h = 0xCBF29CE484222325ULL;
for (int i = 0; i < 8; ++i) {
h ^= bytes[i];
h *= 0x100000001B3ULL;
}
return h;
}
JsonNode BuildKernel(KernelState* kstate, uint32_t entry_pc) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
auto objects = kstate->object_table()->GetAllObjects();
// Sort by semantic id for set-equivalence.
struct OneObj {
uint64_t sid;
JsonNode node;
};
std::vector<OneObj> entries;
for (auto& o : objects) {
uint32_t tc = TypeCode(o->type());
uint32_t rh = o->handle();
uint64_t sid = StableObjectId(tc, rh);
auto n = JsonNode::Object();
n.Set("handle_semantic_id", JsonNode::String(fmt::format("{:016x}", sid)));
n.Set("raw_handle_id", JsonNode::Raw(Hex32(rh)));
n.Set("type", JsonNode::String(TypeName(o->type())));
n.Set("type_code", JsonNode::Unsigned(tc));
n.Set("name", o->name().empty() ? JsonNode::Null()
: JsonNode::String(o->name()));
auto details = JsonNode::Object();
if (o->type() == XObject::Type::Thread) {
auto* th = reinterpret_cast<XThread*>(o.get());
details.Set("thread_id", JsonNode::Unsigned(th->thread_id()));
details.Set("is_entry_thread",
JsonNode::Boolean(
th->main_thread() ||
(th->creation_params() &&
th->creation_params()->start_address == entry_pc)));
details.Set("priority", JsonNode::Integer(th->priority()));
details.Set(
"stack_size",
JsonNode::Unsigned(th->creation_params()
? th->creation_params()->stack_size
: 0));
details.Set("entry_pc",
JsonNode::Raw(Hex32(th->creation_params()
? th->creation_params()->start_address
: 0)));
details.Set("ctx_ptr",
JsonNode::Raw(Hex32(th->creation_params()
? th->creation_params()->start_context
: 0)));
details.Set("suspended", JsonNode::Boolean(false));
}
n.Set("details", std::move(details));
entries.push_back({sid, std::move(n)});
}
std::sort(entries.begin(), entries.end(),
[](const OneObj& a, const OneObj& b) { return a.sid < b.sid; });
std::vector<JsonNode> obj_arr;
obj_arr.reserve(entries.size());
for (auto& e : entries) obj_arr.push_back(std::move(e.node));
root.Set("objects", JsonNode::Array(std::move(obj_arr)));
// We don't enumerate handle_name_table / notification_listeners /
// exports — accessors are not public. Emit empty arrays so the diff
// tool's structural check still has the field present.
root.Set("handle_name_table", JsonNode::Array({}));
root.Set("notification_listeners", JsonNode::Array({}));
root.Set("exports_registered_count", JsonNode::Unsigned(0));
root.Set("exports_registered_sample", JsonNode::Array({}));
root.Set("exports_registered_sha256",
JsonNode::String(std::string(64, '0')));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("raw_handle_id"));
det_skip.push_back(JsonNode::String("exports_registered_count"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- vfs.json ----------
JsonNode BuildVfs(KernelState* kstate) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
auto* fs = kstate->file_system();
// VirtualFileSystem doesn't expose its `devices_` vector or `symlinks_`
// map publicly. To stay additive (no canary-core API surface changes),
// we probe a canonical set of paths via ResolvePath and report only
// what we can observe. Diff tool sorts mounts_observed by path.
std::vector<std::string> probe_paths = {
"\\Device\\Cdrom0",
"\\Device\\Cdrom0\\default.xex",
"\\Device\\Cdrom0\\dat",
"\\Device\\Cdrom0\\dat\\movie",
"\\Device\\Cdrom0\\dat\\movie\\opening.bik",
"game:\\default.xex",
"game:\\dat",
"cache:\\",
"cache:\\nonexistent_probe",
"\\Device\\HardDisk0\\Partition1",
};
std::sort(probe_paths.begin(), probe_paths.end());
std::vector<JsonNode> probes;
for (const auto& path : probe_paths) {
auto entry = fs->ResolvePath(path);
auto o = JsonNode::Object();
o.Set("path", JsonNode::String(path));
o.Set("resolved", JsonNode::Boolean(entry != nullptr));
if (entry) {
o.Set("is_directory",
JsonNode::Boolean((entry->attributes() & 0x10) != 0)); // FILE_ATTR_DIRECTORY
o.Set("size", JsonNode::Unsigned(entry->size()));
} else {
o.Set("is_directory", JsonNode::Null());
o.Set("size", JsonNode::Null());
}
probes.push_back(std::move(o));
}
root.Set("resolve_path_probes", JsonNode::Array(std::move(probes)));
// Mounts observed: report only what `ResolvePath` saw against the
// device prefixes we know about. The data is derived, not enumerated,
// so this is safe under future-canary device additions.
root.Set("mounted_devices_observed_count",
JsonNode::Unsigned(
(fs->ResolvePath("\\Device\\Cdrom0") != nullptr ? 1u : 0u)));
root.Set("cache_root_listing", JsonNode::Array({}));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_path_realpath"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- config.json ----------
JsonNode BuildConfig(KernelState* kstate, uint32_t entry_pc) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
root.Set("build_id", JsonNode::String("canary-phaseB"));
auto exec_module = kstate->GetExecutableModule();
uint32_t image_base = 0;
uint32_t image_size = 0;
std::string image_loaded_sha = std::string(64, '0');
std::string xex_header_sha = std::string(64, '0');
std::string iso_path_str;
if (exec_module) {
image_base = exec_module->xex_module()->base_address();
image_size = exec_module->xex_module()->image_size();
iso_path_str = exec_module->path();
uint8_t* host =
kstate->memory()->TranslateVirtual<uint8_t*>(image_base);
if (host && image_size > 0) {
image_loaded_sha = Sha256Hex(host, image_size);
}
if (exec_module->hash()) {
xex_header_sha = fmt::format("{:016x}", *exec_module->hash());
}
}
root.Set("iso_path", JsonNode::String(iso_path_str));
root.Set("xex_entry_point", JsonNode::Raw(Hex32(entry_pc)));
root.Set("xex_image_base", JsonNode::Raw(Hex32(image_base)));
root.Set("xex_image_size", JsonNode::Unsigned(image_size));
root.Set("image_loaded_sha256", JsonNode::String(image_loaded_sha));
root.Set("xex_header_sha256", JsonNode::String(xex_header_sha));
auto cvars = JsonNode::Object();
cvars.Set("phase_b_snapshot_dir",
JsonNode::String(cvars::phase_b_snapshot_dir));
cvars.Set("phase_b_snapshot_and_exit",
JsonNode::Boolean(cvars::phase_b_snapshot_and_exit));
cvars.Set("phase_b_dump_section_content",
JsonNode::Boolean(cvars::phase_b_dump_section_content));
cvars.Set("phase_a_event_log_path",
JsonNode::String(cvars::phase_a_event_log_path));
root.Set("cvars", std::move(cvars));
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
// wall_clock_iso8601 is non-deterministic; intended for human reading
// only. Diff tool skips it.
std::string wall = fmt::format("epoch:{}", static_cast<int64_t>(t));
root.Set("wall_clock_iso8601", JsonNode::String(wall));
root.Set("host_ns_at_snapshot", JsonNode::Unsigned(0));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_ns_at_snapshot"));
det_skip.push_back(JsonNode::String("wall_clock_iso8601"));
det_skip.push_back(JsonNode::String("build_id"));
det_skip.push_back(JsonNode::String("iso_path"));
det_skip.push_back(JsonNode::String("cvars.phase_b_snapshot_dir"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
void EmitFile(const std::filesystem::path& dir, const char* name,
const JsonNode& node, std::map<std::string, std::string>& hashes) {
std::string body;
node.Serialize(body, 0);
body.push_back('\n');
std::filesystem::path p = dir / name;
std::string h = WriteFileAndHash(p, body);
hashes[name] = h;
}
void WriteSnapshot(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_pc) {
auto* kstate = xthread->kernel_state();
std::filesystem::path base(cvars::phase_b_snapshot_dir);
std::filesystem::path engine_dir = base / "canary";
std::error_code ec;
std::filesystem::create_directories(engine_dir, ec);
std::map<std::string, std::string> hashes;
EmitFile(engine_dir, "cpu_state.json",
BuildCpuState(xthread, thread_state, entry_pc), hashes);
EmitFile(engine_dir, "memory.json",
BuildMemory(kstate, cvars::phase_b_dump_section_content), hashes);
EmitFile(engine_dir, "kernel.json", BuildKernel(kstate, entry_pc), hashes);
EmitFile(engine_dir, "vfs.json", BuildVfs(kstate), hashes);
EmitFile(engine_dir, "config.json", BuildConfig(kstate, entry_pc), hashes);
auto manifest = JsonNode::OrderedObject();
manifest.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
manifest.Set("engine", JsonNode::String(kEngineName));
// Files object is sorted by key (alphabetic), matching the diff tool's
// assumption.
auto files = JsonNode::Object();
for (const auto& [name, hash] : hashes) {
files.Set(name, JsonNode::String(hash));
}
manifest.Set("files", std::move(files));
std::string body;
manifest.Serialize(body, 0);
body.push_back('\n');
std::filesystem::path mp = engine_dir / "manifest.json";
std::FILE* f = std::fopen(mp.string().c_str(), "wb");
if (f) {
std::fwrite(body.data(), 1, body.size(), f);
std::fflush(f);
std::fclose(f);
}
// Phase C: when dump_section_content is on, write raw bytes of the
// XEX image region to <engine_dir>/image.bin. This is the only
// region positionally matched between canary and ours, so it's the
// only one suitable for byte-level diff.
if (cvars::phase_b_dump_section_content) {
auto exec_module = kstate->GetExecutableModule();
if (exec_module) {
uint32_t image_base = exec_module->xex_module()->base_address();
uint32_t image_size = exec_module->xex_module()->image_size();
uint8_t* host =
kstate->memory()->TranslateVirtual<uint8_t*>(image_base);
if (host && image_size > 0) {
std::filesystem::path ip = engine_dir / "image.bin";
std::FILE* bf = std::fopen(ip.string().c_str(), "wb");
if (bf) {
std::fwrite(host, 1, image_size, bf);
std::fflush(bf);
std::fclose(bf);
}
}
}
}
}
} // namespace
void FireIfEntryThread(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_address) {
// Fast path: cvar empty → zero overhead. The .empty() check is a
// single read of a std::string's size, no syscall.
if (cvars::phase_b_snapshot_dir.empty()) {
return;
}
if (g_done.load(std::memory_order_acquire)) {
return;
}
// Resolve the entry_point of the executable module. If it doesn't
// match this thread's first instruction, this isn't the entry thread
// — release any claim we may have made and return.
auto* kstate = xthread ? xthread->kernel_state() : nullptr;
if (!kstate) return;
auto exec_module = kstate->GetExecutableModule();
if (!exec_module) return;
uint32_t entry_pc = exec_module->entry_point();
if (entry_address != entry_pc) return;
// CAS-claim. Releases on guard-fail (above) so a non-entry thread
// reaching its first instruction before the boot thread doesn't
// steal the shot.
bool expected = false;
if (!g_claimed.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
return;
}
WriteSnapshot(xthread, thread_state, entry_pc);
g_done.store(true, std::memory_order_release);
if (cvars::phase_b_snapshot_and_exit) {
std::_Exit(0);
}
}
} // namespace phase_b
} // namespace kernel
} // namespace xe

View File

@@ -0,0 +1,43 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Phase B initial-state snapshot. Cvar-gated (default off).
* Spec: xenia-rs/audit-runs/phase-b-state-equivalence/
******************************************************************************
*/
#ifndef XENIA_KERNEL_PHASE_B_SNAPSHOT_H_
#define XENIA_KERNEL_PHASE_B_SNAPSHOT_H_
#include <cstdint>
namespace xe {
namespace cpu {
class ThreadState;
} // namespace cpu
namespace kernel {
class XThread;
namespace phase_b {
// Called immediately before the JIT executes the first guest PPC
// instruction of a thread. Returns silently when:
// * phase_b_snapshot_dir cvar is empty (zero overhead — default off);
// * a snapshot has already been written (one-shot CAS guard);
// * `entry_address` does not match the loaded executable module's
// entry_point (this thread is not the entry thread — a worker
// spawned by an early kernel call could reach its first instruction
// before the boot thread does).
//
// On a match: writes <dir>/canary/{cpu_state,memory,kernel,vfs,config}.json
// + manifest.json, optionally `_Exit(0)` per phase_b_snapshot_and_exit.
void FireIfEntryThread(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_address);
} // namespace phase_b
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_PHASE_B_SNAPSHOT_H_