Merge pull request '[Audit] bring the audit_61 branch probe onto sylpheed-re' (#2) from fix/audit61-onto-sylpheed-re into sylpheed-re
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m26s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-09-21 16:03:10 +00:00
4 changed files with 448 additions and 19 deletions

View File

@@ -13,6 +13,8 @@
#include <climits> #include <climits>
#include <cstring> #include <cstring>
#include <string>
#include <vector>
#include "third_party/fmt/include/fmt/format.h" #include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/assert.h" #include "xenia/base/assert.h"
@@ -87,6 +89,111 @@ DEFINE_bool(instrument_call_times, false,
"Compute time taken for functions, for profiling guest code", "Compute time taken for functions, for profiling guest code",
"x64"); "x64");
#endif #endif
// AUDIT-061/067: forward decls of probe/watch tables (defined in
// ppc_hir_builder.cc).
namespace xe {
namespace cpu {
namespace audit61 {
const std::vector<uint32_t>& pcs();
} // namespace audit61
namespace audit67 {
const std::vector<uint32_t>& vals();
} // namespace audit67
namespace hashprobe {
const std::vector<uint32_t>& pcs();
} // namespace hashprobe
} // namespace cpu
} // namespace xe
// AUDIT-061: handler for trap codes [200, 232). arg0 carries trap idx
// (trap_code - 200), mapping to ::xe::cpu::audit61::pcs()[idx]. Emits one
// log line per fire with cr0/cr6 LGE flags + key GPRs + LR + tid.
static uint64_t TrapAudit61Branch(void* raw_context, uint64_t idx) {
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
const auto& pcs = ::xe::cpu::audit61::pcs();
uint32_t pc = (idx < pcs.size()) ? pcs[static_cast<size_t>(idx)] : 0u;
uint32_t tid = 0;
if (ctx->thread_state) {
tid = ctx->thread_state->thread_id();
}
auto enc = [](uint8_t lt, uint8_t gt, uint8_t eq) {
char buf[4];
buf[0] = lt ? 'L' : '.';
buf[1] = gt ? 'G' : '.';
buf[2] = eq ? 'E' : '.';
buf[3] = '\0';
return std::string(buf);
};
XELOGI(
"AUDIT-061-BR pc={:08X} lr={:08X} cr0={} cr6={} r3={:08X} r4={:08X} "
"r5={:08X} r6={:08X} r31={:08X} tid={}",
pc, static_cast<uint32_t>(ctx->lr),
enc(ctx->cr0.cr0_lt, ctx->cr0.cr0_gt, ctx->cr0.cr0_eq),
enc(ctx->cr6.cr6_all_equal, ctx->cr6.cr6_1, ctx->cr6.cr6_none_equal),
static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
static_cast<uint32_t>(ctx->r[31]), tid);
return 0;
}
// AUDIT-067: handler for trap codes [250, 254). arg0 carries trap idx
// (trap_code - 250), mapping to ::xe::cpu::audit67::vals()[idx]. Fired when
// a 4-byte guest store sees the configured value. The store-emit site stashed
// (pc << 32) | (ea & 0xFFFFFFFF) into ctx->scratch right before the trap.
static uint64_t TrapAudit67ValueWatch(void* raw_context, uint64_t idx) {
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
const auto& vals = ::xe::cpu::audit67::vals();
uint32_t val =
(idx < vals.size()) ? vals[static_cast<size_t>(idx)] : 0u;
uint32_t pc = static_cast<uint32_t>(ctx->scratch >> 32);
uint32_t dst = static_cast<uint32_t>(ctx->scratch & 0xFFFFFFFFu);
uint32_t tid = 0;
if (ctx->thread_state) {
tid = ctx->thread_state->thread_id();
}
XELOGI(
"AUDIT-067-VAL pc={:08X} lr={:08X} val={:08X} dst={:08X} "
"r3={:08X} r4={:08X} r5={:08X} r6={:08X} r31={:08X} tid={}",
pc, static_cast<uint32_t>(ctx->lr), val, dst,
static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
static_cast<uint32_t>(ctx->r[31]), tid);
return 0;
}
// PHASE-A hash probe: trap codes [300, 332). arg0 carries trap idx
// (trap_code - 300) -> ::xe::cpu::hashprobe::pcs()[idx]. Derefs r3 as a guest
// C-string (the archive path being hashed) and logs it with r3..r6 + LR + tid.
// Log-only; observes the same code path the retail binary runs.
static uint64_t TrapPhaseAHashProbe(void* raw_context, uint64_t idx) {
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
const auto& pcs = ::xe::cpu::hashprobe::pcs();
uint32_t pc = (idx < pcs.size()) ? pcs[static_cast<size_t>(idx)] : 0u;
uint32_t r3 = static_cast<uint32_t>(ctx->r[3]);
// Deref r3 as a guest C-string via the flat virtual mapping (bounded).
std::string arg;
if (r3 && ctx->virtual_membase) {
const char* p =
reinterpret_cast<const char*>(ctx->virtual_membase) + r3;
for (size_t i = 0; i < 128 && p[i]; ++i) {
char c = p[i];
arg.push_back((c >= 0x20 && c < 0x7f) ? c : '.');
}
}
uint32_t tid = 0;
if (ctx->thread_state) {
tid = ctx->thread_state->thread_id();
}
XELOGI(
"PHASE-A-HASHPROBE pc={:08X} lr={:08X} arg=\"{}\" r3={:08X} r4={:08X} "
"r5={:08X} r6={:08X} tid={}",
pc, static_cast<uint32_t>(ctx->lr), arg, r3,
static_cast<uint32_t>(ctx->r[4]), static_cast<uint32_t>(ctx->r[5]),
static_cast<uint32_t>(ctx->r[6]), tid);
return 0;
}
namespace xe { namespace xe {
namespace cpu { namespace cpu {
namespace backend { namespace backend {
@@ -566,6 +673,27 @@ void X64Emitter::Trap(uint16_t trap_type) {
// ? // ?
break; break;
default: default:
// AUDIT-067: trap codes [250, 254) dispatch the value-watch handler.
// arg0 = idx into ::xe::cpu::audit67::vals().
if (trap_type >= 250 && trap_type < 254) {
CallNative(::TrapAudit67ValueWatch,
static_cast<uint64_t>(trap_type - 250));
break;
}
// AUDIT-061: trap codes [200, 232) dispatch the branch-probe handler.
// arg0 = idx into ::xe::cpu::audit61::pcs().
if (trap_type >= 200 && trap_type < 232) {
CallNative(::TrapAudit61Branch,
static_cast<uint64_t>(trap_type - 200));
break;
}
// PHASE-A hash probe: trap codes [300, 332).
// arg0 = idx into ::xe::cpu::hashprobe::pcs().
if (trap_type >= 300 && trap_type < 332) {
CallNative(::TrapPhaseAHashProbe,
static_cast<uint64_t>(trap_type - 300));
break;
}
XELOGW("Unknown trap type {}", trap_type); XELOGW("Unknown trap type {}", trap_type);
db(0xCC); db(0xCC);
break; break;

View File

@@ -58,8 +58,76 @@ DEFINE_bool(break_condition_truncate, true, "truncate value to 32-bits", "CPU");
DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.", DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.",
"CPU"); "CPU");
// Phase A — expansive extraction tracing (game-data RE); see kernel/event_log.h. // AUDIT-DEMO: smoke marker (memory entry: emulator.cc:225,283). Always-on bool.
// All default-off so instrument-current behaviour is unchanged when unused. DEFINE_bool(audit_demo_setup_trace, true,
"Audit smoke marker: log AUDIT-DEMO-SETUP-BEGIN at emulator setup.",
"Audit");
// AUDIT-061: comma-separated list of guest PCs to log on each fire.
// Format: "0xPC1,0xPC2,..." (max 32 PCs). Each fire emits
// AUDIT-061-BR pc=X lr=X cr0=LGE cr6=LGE r3=X r4=X r5=X r6=X r31=X tid=N.
// Default empty (off); no perf cost when empty.
DEFINE_string(audit_61_branch_probe_pcs, "",
"AUDIT-061: CSV of guest PCs to trace (cr0/cr6 + regs/tid).",
"Audit");
// AUDIT-067: comma-separated list of u32 values to watch. When non-empty,
// every 4-byte guest store (stw/stwu/stwx/stwux/stmw) emits a runtime
// equality check; matches log AUDIT-067-VAL pc=X lr=X val=X dst=X r3..r6 r31 tid=N.
// Max 4 values. Default empty (off); zero overhead when empty.
DEFINE_string(audit_67_value_watch, "",
"AUDIT-067: CSV of u32 values (max 4) — log every guest "
"store whose value matches.",
"Audit");
// AUDIT-068: host-side memory-write watch. See cpu_flags.h header for format.
// Mirrors AUDIT-067 but covers host-side writes (xe::store_and_swap<T>,
// Memory::Zero/Fill/Copy). Empty default = zero cost.
DEFINE_string(audit_68_host_mem_watch_values, "",
"AUDIT-068: CSV of u32 values (max 8) — log every host-side "
"guest-memory write whose value matches.",
"Audit");
DEFINE_string(audit_68_host_mem_watch_addrs, "",
"AUDIT-068: CSV of guest VAs or VA ranges 'START-END' (max 8) "
"— log every host-side guest-memory write whose guest VA falls "
"within the configured set.",
"Audit");
// AUDIT-068 Session 3: read-mode probe. See cpu_flags.h for format.
DEFINE_string(audit_68_host_mem_read_probe, "",
"AUDIT-068 Session 3: CSV of 'VA:SIZE:PERIOD_NS' tuples (max 8) "
"— a dedicated poll thread reads the value at each VA every "
"PERIOD_NS and emits AUDIT-068-READ-CHANGE on transition.",
"Audit");
// AUDIT-069: see cpu_flags.h header. Empty default = zero cost.
DEFINE_string(audit_69_event_signal_watch, "",
"AUDIT-069: CSV of guest event-handle IDs (max 4) — log each "
"XEvent::Set / Ke*Event / Nt*Event fire whose target matches.",
"Audit");
DEFINE_string(audit_69_event_signal_native_ptr, "",
"AUDIT-069: CSV of guest event native VAs (X_KEVENT*) (max 4) "
"— log each set fire whose native pointer matches.",
"Audit");
DEFINE_bool(audit_69_log_all_sets, false,
"AUDIT-069: when true, log EVERY XEvent::Set/Pulse fire (used "
"for one-run wait→signal correlation across handle drift). "
"Default false; use only with --mute=true.",
"Audit");
// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. See header.
DEFINE_string(audit_70_semaphore_release_watch, "",
"AUDIT-070: CSV of guest semaphore handle IDs (max 4) — log "
"each NtReleaseSemaphore / xeKeReleaseSemaphore fire whose "
"target matches.",
"Audit");
DEFINE_bool(audit_70_log_all_releases, false,
"AUDIT-070: when true, log EVERY NtReleaseSemaphore / "
"xeKeReleaseSemaphore fire (used to identify the work-semaphore "
"handle on first run). Default false; use only with --mute=true.",
"Audit");
// Phase A — see kernel/event_log.h.
DEFINE_string(phase_a_event_log_path, "", DEFINE_string(phase_a_event_log_path, "",
"Phase A: write schema-v1 JSONL event log to this path. " "Phase A: write schema-v1 JSONL event log to this path. "
"Empty (default) = disabled.", "Empty (default) = disabled.",
@@ -68,30 +136,38 @@ DEFINE_bool(phase_a_event_log_mem_writes, false,
"Phase A: include mem.write events in the JSONL log. RESERVED — " "Phase A: include mem.write events in the JSONL log. RESERVED — "
"not wired in this phase. Default false.", "not wired in this phase. Default false.",
"Audit"); "Audit");
DEFINE_bool(phase_a_trace_args, false,
"Phase A extraction: populate the kernel.call args (raw r3..r10) and " // Phase A — expansive extraction tracing (game-data RE). All default-off so the
"args_resolved (file I/O path/offset/length/buffer, alloc size, " // diff-oriented schema-v1 output stays byte-identical when unused.
"handle names) fields, and emit file.read events. Default false.",
"Audit");
DEFINE_bool(phase_a_fileio_only, false, DEFINE_bool(phase_a_fileio_only, false,
"Phase A extraction (LIGHTWEIGHT): emit ONLY file opens (path) and " "Phase A extraction (LIGHTWEIGHT): emit ONLY file opens (path) and "
"file.read events (path/offset/length) — no per-export import/kernel " "file.read events (path/offset/length) — no per-export import/kernel "
"call spam. Use to trace which sound.pak/movie bytes a title reads " "call spam. Use to trace which sound.pak/movie bytes a title reads "
"(e.g. movie->voice mapping) without perturbing timing. Default false.", "(e.g. movie->voice mapping) without perturbing timing. Default false.",
"Audit"); "Audit");
DEFINE_bool(phase_a_trace_args, false,
"Phase A extraction: populate the kernel.call args (raw r3..r10) and "
"args_resolved (file I/O path/offset/length/buffer, alloc size, "
"handle names) fields, and emit file.read events. Default false.",
"Audit");
DEFINE_string(phase_a_hash_probe, "", DEFINE_string(phase_a_hash_probe, "",
"Phase A extraction: CSV of guest PCs — RESERVED (the HIR/emitter " "Phase A extraction: CSV of guest PCs (max 32). At each, deref r3 "
"trap that drives it is not ported to instrument-current). Inert. " "as a guest C-string and emit a guest.call event {pc,arg_str,"
"Default empty (off).", "r3..r6,tid} — used to recover the IPFB/IDXD name-hash. Default "
"empty (off).",
"Audit"); "Audit");
// Phase D Stage 1 — see kernel/event_log.h `EmitContentionObserved`.
DEFINE_bool(kernel_emit_contention, false, DEFINE_bool(kernel_emit_contention, false,
"Phase D Stage 1: emit `contention.observed` events. Default false " "Phase D Stage 1: emit `contention.observed` events when "
"(zero cost when disabled). Requires --phase_a_event_log_path.", "RtlEnterCriticalSection's spin loop is exhausted and the call "
"falls through to xeKeWaitForSingleObject. Default false (zero "
"cost when disabled). Requires --phase_a_event_log_path to be "
"set as well.",
"Audit"); "Audit");
// Phase B — see kernel/phase_b_snapshot.h. Ported alongside the snapshot // Phase B — see kernel/phase_b_snapshot.h.
// probe itself, which was taken as files from
// auto/canary-instrumentation-snapshot-2026-07-28 rather than merged.
DEFINE_string(phase_b_snapshot_dir, "", DEFINE_string(phase_b_snapshot_dir, "",
"Phase B: write 5-file structured state snapshot to " "Phase B: write 5-file structured state snapshot to "
"<dir>/canary/ at the moment immediately before the first " "<dir>/canary/ at the moment immediately before the first "

View File

@@ -35,14 +35,78 @@ DECLARE_bool(break_condition_truncate);
DECLARE_bool(break_on_debugbreak); DECLARE_bool(break_on_debugbreak);
// Phase A — expansive extraction tracing; see kernel/event_log.h. // AUDIT-DEMO smoke marker.
DECLARE_bool(audit_demo_setup_trace);
// AUDIT-061: multi-PC branch probe — emits one log line per fire with
// (pc, lr, cr0 LGE, cr6 LGE, r3, r4, r5, r6, r31, tid). CSV of guest PCs.
DECLARE_string(audit_61_branch_probe_pcs);
// AUDIT-067: value-watch — emit a log line for each 32-bit guest store whose
// value-to-be-stored matches any configured value. CSV of u32 values
// ("0xDEADBEEF,..."), max 4 entries. Default empty (off); zero cost when empty.
DECLARE_string(audit_67_value_watch);
// AUDIT-068: host-side memory-write watch — emit a log line for each host-side
// write to guest memory whose VALUE matches any configured u32 value, or whose
// guest VA falls within any configured ADDR or ADDR-range. Mirrors AUDIT-067
// but covers the host-side write paths (xe::store_and_swap<T>, Memory::Zero/
// Fill/Copy) that AUDIT-067's JIT store-opcode hooks cannot see.
//
// VALUES: CSV of u32 values, max 8 entries; e.g. "0x8200A208,0x8200A928".
// ADDRS: CSV of guest VAs or VA ranges, max 8 entries; range form is
// "0xSTART-0xEND" (inclusive). e.g. "0x42500000-0x42600000,0xBCE25340".
// Default empty (off); zero cost on the hot path when both are empty.
DECLARE_string(audit_68_host_mem_watch_values);
DECLARE_string(audit_68_host_mem_watch_addrs);
// AUDIT-068 Session 3: read-mode probe. CSV of "VA:SIZE:PERIOD_NS" tuples
// (max 8). A dedicated low-priority thread polls each VA every PERIOD_NS and
// emits AUDIT-068-READ-CHANGE when the value transitions. SIZE in {1,2,4,8}.
// Example: "0xBCE25340:4:1000000" = poll u32 at 0xBCE25340 every 1 ms.
// Default empty (off); the poll thread is not spawned when empty.
DECLARE_string(audit_68_host_mem_read_probe);
// AUDIT-069: event-signal watch. CSV of guest handle IDs (e.g. "0xF8000098")
// to log on every XEvent::Set / KeSetEvent / NtSetEvent / KePulseEvent /
// NtPulseEvent fire whose target matches. Max 4 entries. Default empty (off);
// zero cost on the hot path when empty.
DECLARE_string(audit_69_event_signal_watch);
// AUDIT-069: event-signal watch by native guest VA (X_KEVENT*). CSV of guest
// VAs (max 4). Default empty (off). Use when the handle id varies across
// boots but the native dispatcher pointer is stable.
DECLARE_string(audit_69_event_signal_native_ptr);
// AUDIT-069: when true, log EVERY XEvent::Set / XEvent::Pulse fire (subject
// to the slowpath gate). Use only with --mute=true and short windows — high
// volume. Default false (off).
DECLARE_bool(audit_69_log_all_sets);
// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. CSV of guest
// handle IDs (e.g. "0xF8000098") to log on every NtReleaseSemaphore /
// xeKeReleaseSemaphore fire whose target matches. Max 4 entries. Default
// empty (off); zero cost on the hot path when empty.
DECLARE_string(audit_70_semaphore_release_watch);
// AUDIT-070: when true, log EVERY NtReleaseSemaphore / xeKeReleaseSemaphore
// fire. Use only with --mute=true and short windows — used to identify the
// canary work-semaphore handle on first run. Default false (off).
DECLARE_bool(audit_70_log_all_releases);
// Phase A: JSONL event-log emitter path. When non-empty, the engine writes
// schema-v1 JSONL events to this file. Empty (default) = no overhead, no
// behavior change. Schema: xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md
DECLARE_string(phase_a_event_log_path); DECLARE_string(phase_a_event_log_path);
DECLARE_bool(phase_a_event_log_mem_writes); DECLARE_bool(phase_a_event_log_mem_writes);
DECLARE_bool(phase_a_trace_args);
DECLARE_string(phase_a_hash_probe);
DECLARE_bool(kernel_emit_contention); DECLARE_bool(kernel_emit_contention);
// Phase B — structured state snapshot; see kernel/phase_b_snapshot.h. DECLARE_bool(phase_a_trace_args);
DECLARE_string(phase_a_hash_probe);
// Phase B: initial-state snapshot. When the dir cvar is non-empty, the
// engine writes a five-file structured state snapshot (cpu_state.json,
// memory.json, kernel.json, vfs.json, config.json, plus manifest.json) to
// `<dir>/canary/` at the moment immediately before the first guest PPC
// instruction of the XEX entry_point executes. See
// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
DECLARE_string(phase_b_snapshot_dir); DECLARE_string(phase_b_snapshot_dir);
DECLARE_bool(phase_b_snapshot_and_exit); DECLARE_bool(phase_b_snapshot_and_exit);
DECLARE_bool(phase_b_dump_section_content); DECLARE_bool(phase_b_dump_section_content);

View File

@@ -34,6 +34,141 @@ DEFINE_bool(
"unimplemented PowerPC instruction is encountered.", "unimplemented PowerPC instruction is encountered.",
"CPU"); "CPU");
// AUDIT-061 — multi-PC branch probe. Parses cvars::audit_61_branch_probe_pcs
// once and exposes a (pc -> trap_id) lookup table. trap_id range [200, 65535].
// PCs outside the table are not probed. Native side reads g_audit61_pcs[idx].
#include <vector>
#include <string>
namespace xe {
namespace cpu {
namespace audit61 {
constexpr uint16_t kTrapBase = 200;
constexpr size_t kMaxPcs = 32;
static std::vector<uint32_t> g_pcs;
static bool g_parsed = false;
const std::vector<uint32_t>& pcs() {
if (!g_parsed) {
g_parsed = true;
const std::string& csv = cvars::audit_61_branch_probe_pcs;
size_t pos = 0;
while (pos < csv.size() && g_pcs.size() < kMaxPcs) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
// strip whitespace
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
if (!tok.empty()) {
try {
uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
g_pcs.push_back(v);
} catch (...) {
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
return g_pcs;
}
// Returns trap id for pc, or 0 if pc not in probe set.
uint16_t trap_id_for(uint32_t pc) {
const auto& v = pcs();
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == pc) return static_cast<uint16_t>(kTrapBase + i);
}
return 0;
}
} // namespace audit61
// AUDIT-067 — value-watch. Parses cvars::audit_67_value_watch once, exposes
// values via vals(). Trap codes for matches start at kTrapBase = 250.
namespace audit67 {
constexpr uint16_t kTrapBase = 250;
constexpr size_t kMaxVals = 4;
static std::vector<uint32_t> g_vals;
static bool g_parsed = false;
const std::vector<uint32_t>& vals() {
if (!g_parsed) {
g_parsed = true;
const std::string& csv = cvars::audit_67_value_watch;
size_t pos = 0;
while (pos < csv.size() && g_vals.size() < kMaxVals) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
if (!tok.empty()) {
try {
uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
g_vals.push_back(v);
} catch (...) {
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
XELOGI("AUDIT-067-INIT csv=\"{}\" parsed_count={}", csv, g_vals.size());
for (size_t i = 0; i < g_vals.size(); ++i) {
XELOGI("AUDIT-067-INIT vals[{}] = 0x{:08X}", i, g_vals[i]);
}
}
return g_vals;
}
} // namespace audit67
// PHASE-A hash probe — multi-PC guest probe for recovering the IPFB/IDXD
// name-hash. Parses cvars::phase_a_hash_probe once; trap ids start at 300.
// At each fire the native handler derefs r3 as a guest C-string (the archive
// path being hashed) and logs it alongside r3..r6. Mirrors audit61.
namespace hashprobe {
constexpr uint16_t kTrapBase = 300;
constexpr size_t kMaxPcs = 32;
static std::vector<uint32_t> g_pcs;
static bool g_parsed = false;
const std::vector<uint32_t>& pcs() {
if (!g_parsed) {
g_parsed = true;
const std::string& csv = cvars::phase_a_hash_probe;
size_t pos = 0;
while (pos < csv.size() && g_pcs.size() < kMaxPcs) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
if (!tok.empty()) {
try {
g_pcs.push_back(static_cast<uint32_t>(std::stoul(tok, nullptr, 0)));
} catch (...) {
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
return g_pcs;
}
uint16_t trap_id_for(uint32_t pc) {
const auto& v = pcs();
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == pc) return static_cast<uint16_t>(kTrapBase + i);
}
return 0;
}
} // namespace hashprobe
} // namespace cpu
} // namespace xe
namespace xe { namespace xe {
namespace cpu { namespace cpu {
namespace ppc { namespace ppc {
@@ -174,6 +309,32 @@ bool PPCHIRBuilder::Emit(GuestFunction* function, uint32_t flags) {
MaybeBreakOnInstruction(address); MaybeBreakOnInstruction(address);
// AUDIT-061: emit a trap before this instruction if it's on the probe
// list. The trap fires BEFORE the cmp/branch HIR emit so the native
// handler observes cr0/cr6 set by the *previous* instruction (the cmp
// that controls this conditional branch). ContextBarrier flushes
// HIR temporaries to PPCContext so the handler reads consistent state.
if (!::xe::cpu::audit61::pcs().empty()) {
uint16_t tid = ::xe::cpu::audit61::trap_id_for(address);
if (tid != 0) {
Comment("--audit_61_branch_probe target");
ContextBarrier();
Trap(tid);
}
}
// PHASE-A hash probe: trap before this instruction so the native handler
// observes r3 (arg string ptr) as set by the caller. ContextBarrier
// flushes HIR temporaries so the handler reads consistent GPRs.
if (!::xe::cpu::hashprobe::pcs().empty()) {
uint16_t tid = ::xe::cpu::hashprobe::trap_id_for(address);
if (tid != 0) {
Comment("--phase_a_hash_probe target");
ContextBarrier();
Trap(tid);
}
}
InstrData i; InstrData i;
i.address = address; i.address = address;
i.code = code; i.code = code;