From fcdc2659fc0225eaf0abcf903d5f92b92694f34e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 20:43:27 +0200 Subject: [PATCH] [Audit] bring the audit_61 branch probe onto sylpheed-re Fork issue #1: `--audit_61_branch_probe_pcs` existed only on `phase-a-tracing` and the 2026-07-28 snapshot, so `/sylph-canary` -- which drives probe runs and parses hit counts -- could not work. Measured, not assumed: `strings` on both built binaries gives zero hits for the cvar. MERGE, NOT REBUILD. `phase-a-tracing` is ONE commit past the common ancestor while `sylpheed-re` is 232 commits ahead of it, so rebuilding from that branch would have discarded 232 commits including the RE-INPUT/RE-DRAW instrumentation. Bringing the one commit forward is the cheap direction. Cherry-picked a15430902 (30d05ee97 does NOT introduce the probe -- it is the cross-build snapshot). Three conflicts, each resolved on evidence: * `kernel/event_log.cc` looked like two rival implementations -- 726 lines vs 709 -- but they share 709 lines and differ by 19: ours is a strict SUPERSET, phase-a's file plus a later `phase_a_fileio_only` mode that suppresses per-export log spam. Took ours whole. * `cpu/cpu_flags.{cc,h}`: taken as a verified UNION rather than hand-edited. Theirs contributes 11 cvars (audit_61 plus audit_67/68/69/70 and the demo marker); ours contributes `phase_a_fileio_only` and `kernel_emit_contention`. Asserted afterwards that no flag present on either side is missing from the result -- which caught `kernel_emit_contention`, that a take-theirs would have dropped silently. All ten probe cvars are referenced by the cleanly-merged `x64_emitter.cc` and `ppc_hir_builder.cc`, so they are load-bearing, not decoration. The three `audit_jit_prolog_*` cvars that look undeclared are DEFINE_uint32 in the translation unit that uses them -- pre-existing on sylpheed-re, not this merge. Co-Authored-By: Claude Opus 5 (1M context) --- src/xenia/cpu/backend/x64/x64_emitter.cc | 128 ++++++++++++++++++ src/xenia/cpu/cpu_flags.cc | 106 ++++++++++++--- src/xenia/cpu/cpu_flags.h | 72 +++++++++- src/xenia/cpu/ppc/ppc_hir_builder.cc | 161 +++++++++++++++++++++++ 4 files changed, 448 insertions(+), 19 deletions(-) diff --git a/src/xenia/cpu/backend/x64/x64_emitter.cc b/src/xenia/cpu/backend/x64/x64_emitter.cc index d5592a401..52710a3c1 100644 --- a/src/xenia/cpu/backend/x64/x64_emitter.cc +++ b/src/xenia/cpu/backend/x64/x64_emitter.cc @@ -13,6 +13,8 @@ #include #include +#include +#include #include "third_party/fmt/include/fmt/format.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", "x64"); #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& pcs(); +} // namespace audit61 +namespace audit67 { +const std::vector& vals(); +} // namespace audit67 +namespace hashprobe { +const std::vector& 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(raw_context); + const auto& pcs = ::xe::cpu::audit61::pcs(); + uint32_t pc = (idx < pcs.size()) ? pcs[static_cast(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(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(ctx->r[3]), static_cast(ctx->r[4]), + static_cast(ctx->r[5]), static_cast(ctx->r[6]), + static_cast(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(raw_context); + const auto& vals = ::xe::cpu::audit67::vals(); + uint32_t val = + (idx < vals.size()) ? vals[static_cast(idx)] : 0u; + uint32_t pc = static_cast(ctx->scratch >> 32); + uint32_t dst = static_cast(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(ctx->lr), val, dst, + static_cast(ctx->r[3]), static_cast(ctx->r[4]), + static_cast(ctx->r[5]), static_cast(ctx->r[6]), + static_cast(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(raw_context); + const auto& pcs = ::xe::cpu::hashprobe::pcs(); + uint32_t pc = (idx < pcs.size()) ? pcs[static_cast(idx)] : 0u; + uint32_t r3 = static_cast(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(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(ctx->lr), arg, r3, + static_cast(ctx->r[4]), static_cast(ctx->r[5]), + static_cast(ctx->r[6]), tid); + return 0; +} + namespace xe { namespace cpu { namespace backend { @@ -566,6 +673,27 @@ void X64Emitter::Trap(uint16_t trap_type) { // ? break; 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(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(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(trap_type - 300)); + break; + } XELOGW("Unknown trap type {}", trap_type); db(0xCC); break; diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc index 41051e4b2..49a8cf28d 100644 --- a/src/xenia/cpu/cpu_flags.cc +++ b/src/xenia/cpu/cpu_flags.cc @@ -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.", "CPU"); -// Phase A — expansive extraction tracing (game-data RE); see kernel/event_log.h. -// All default-off so instrument-current behaviour is unchanged when unused. +// AUDIT-DEMO: smoke marker (memory entry: emulator.cc:225,283). Always-on bool. +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, +// 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, "", "Phase A: write schema-v1 JSONL event log to this path. " "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 — " "not wired in this phase. Default false.", "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"); + +// Phase A — expansive extraction tracing (game-data RE). All default-off so the +// diff-oriented schema-v1 output stays byte-identical when unused. DEFINE_bool(phase_a_fileio_only, false, "Phase A extraction (LIGHTWEIGHT): emit ONLY file opens (path) and " "file.read events (path/offset/length) — no per-export import/kernel " "call spam. Use to trace which sound.pak/movie bytes a title reads " "(e.g. movie->voice mapping) without perturbing timing. Default false.", "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, "", - "Phase A extraction: CSV of guest PCs — RESERVED (the HIR/emitter " - "trap that drives it is not ported to instrument-current). Inert. " - "Default empty (off).", + "Phase A extraction: CSV of guest PCs (max 32). At each, deref r3 " + "as a guest C-string and emit a guest.call event {pc,arg_str," + "r3..r6,tid} — used to recover the IPFB/IDXD name-hash. Default " + "empty (off).", "Audit"); + +// Phase D Stage 1 — see kernel/event_log.h `EmitContentionObserved`. DEFINE_bool(kernel_emit_contention, false, - "Phase D Stage 1: emit `contention.observed` events. Default false " - "(zero cost when disabled). Requires --phase_a_event_log_path.", + "Phase D Stage 1: emit `contention.observed` events when " + "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"); -// Phase B — see kernel/phase_b_snapshot.h. Ported alongside the snapshot -// probe itself, which was taken as files from -// auto/canary-instrumentation-snapshot-2026-07-28 rather than merged. +// Phase B — see kernel/phase_b_snapshot.h. DEFINE_string(phase_b_snapshot_dir, "", "Phase B: write 5-file structured state snapshot to " "/canary/ at the moment immediately before the first " diff --git a/src/xenia/cpu/cpu_flags.h b/src/xenia/cpu/cpu_flags.h index 2245c3762..de063b54f 100644 --- a/src/xenia/cpu/cpu_flags.h +++ b/src/xenia/cpu/cpu_flags.h @@ -35,14 +35,78 @@ DECLARE_bool(break_condition_truncate); 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, 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_bool(phase_a_event_log_mem_writes); -DECLARE_bool(phase_a_trace_args); -DECLARE_string(phase_a_hash_probe); 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 +// `/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_bool(phase_b_snapshot_and_exit); DECLARE_bool(phase_b_dump_section_content); diff --git a/src/xenia/cpu/ppc/ppc_hir_builder.cc b/src/xenia/cpu/ppc/ppc_hir_builder.cc index 42d996cba..d25909079 100644 --- a/src/xenia/cpu/ppc/ppc_hir_builder.cc +++ b/src/xenia/cpu/ppc/ppc_hir_builder.cc @@ -34,6 +34,141 @@ DEFINE_bool( "unimplemented PowerPC instruction is encountered.", "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 +#include +namespace xe { +namespace cpu { +namespace audit61 { +constexpr uint16_t kTrapBase = 200; +constexpr size_t kMaxPcs = 32; +static std::vector g_pcs; +static bool g_parsed = false; + +const std::vector& 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(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(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 g_vals; +static bool g_parsed = false; + +const std::vector& 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(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 g_pcs; +static bool g_parsed = false; + +const std::vector& 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(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(kTrapBase + i); + } + return 0; +} +} // namespace hashprobe +} // namespace cpu +} // namespace xe + namespace xe { namespace cpu { namespace ppc { @@ -174,6 +309,32 @@ bool PPCHIRBuilder::Emit(GuestFunction* function, uint32_t flags) { 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; i.address = address; i.code = code;