diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc index 3ff067e15..e6f412f91 100644 --- a/src/xenia/cpu/cpu_flags.cc +++ b/src/xenia/cpu/cpu_flags.cc @@ -57,3 +57,110 @@ DEFINE_bool(break_condition_truncate, true, "truncate value to 32-bits", "CPU"); DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.", "CPU"); + +// 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.", + "Audit"); +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"); + +// Phase D Stage 1 — see kernel/event_log.h `EmitContentionObserved`. +DEFINE_bool(kernel_emit_contention, false, + "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. +DEFINE_string(phase_b_snapshot_dir, "", + "Phase B: write 5-file structured state snapshot to " + "/canary/ at the moment immediately before the first " + "guest PPC instruction of entry_point. Empty (default) = " + "disabled, zero overhead.", + "Audit"); +DEFINE_bool(phase_b_snapshot_and_exit, false, + "Phase B: after writing the snapshot, exit the process " + "immediately (std::_Exit(0)) so re-runs are byte-deterministic.", + "Audit"); +DEFINE_bool(phase_b_dump_section_content, false, + "Phase B: in memory.json, populate section_contents[].content_b64 " + "with raw bytes of every committed XEX-image region. Default " + "false — per-region SHA-256 is enough for the routine diff; " + "this is the escape hatch for the STOP-and-report condition " + "(image_loaded_sha256 mismatch).", + "Audit");diff --git a/src/xenia/cpu/cpu_flags.h b/src/xenia/cpu/cpu_flags.h index 38c4f98ba..95fe8cb22 100644 --- a/src/xenia/cpu/cpu_flags.h +++ b/src/xenia/cpu/cpu_flags.h @@ -35,4 +35,76 @@ DECLARE_bool(break_condition_truncate); DECLARE_bool(break_on_debugbreak); +// 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); + +// 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); + #endif // XENIA_CPU_CPU_FLAGS_H_diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index ced21a600..e1c74d7ec 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -12,6 +12,8 @@ #include "xenia/base/clock.h" #include "xenia/base/platform.h" #include "xenia/cpu/processor.h" +#include "xenia/kernel/audit_70_semaphore_release_watch.h" +#include "xenia/kernel/event_log.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xsemaphore.h" @@ -147,6 +149,25 @@ uint32_t ExCreateThread(xe::be* handle_ptr, uint32_t stack_size, if (thread_id_ptr) { *thread_id_ptr = thread->thread_id(); } + // Phase C+15-α: schema-v1 `thread.create` event. Symmetric with + // ours's `ex_create_thread`. Emitted by the **parent** thread. + // handle.create for the thread handle itself was already emitted + // via ObjectTable::AddHandle inside XThread::Create. Here we + // surface the spawn-specific metadata. + if (phase_a::IsEnabled()) { + uint64_t sid = phase_a::LookupHandleSemanticId(thread->handle()); + XThread* parent = XThread::TryGetCurrentThread(); + uint32_t parent_tid = 0; + if (parent) { + parent_tid = static_cast( + parent->guest_object()->thread_id); + } + uint32_t affinity = (creation_flags >> 24) & 0xFF; + bool suspended = (creation_flags & 0x1) != 0; + phase_a::EmitThreadCreate(sid, parent_tid, start_address, start_context, + /* priority */ 0, affinity, actual_stack_size, + suspended); + } } return result; } @@ -165,6 +186,9 @@ DECLARE_XBOXKRNL_EXPORT1(ExCreateThread, kThreading, kImplemented); uint32_t ExTerminateThread(uint32_t exit_code) { XThread* thread = XThread::GetCurrentThread(); + // Phase C+15-α: schema-v1 `thread.exit` is emitted inside + // `XThread::Exit` (covers both explicit ExTerminateThread and + // implicit thread-entry returns). // NOTE: this kills us right now. We won't return from it. return thread->Exit(exit_code); @@ -718,6 +742,9 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment, int32_t previous_count = 0; [[maybe_unused]] bool success = sem->ReleaseSemaphore(adjustment, &previous_count); + // AUDIT-070: log Ke-form release fires whose target handle matches. + audit_70::check_release(sem->handle(), "xeKeReleaseSemaphore", + static_cast(adjustment), previous_count); return static_cast(previous_count); } @@ -786,6 +813,13 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle, uint32_t(release_count), previous_count); result = X_STATUS_SEMAPHORE_LIMIT_EXCEEDED; } + // AUDIT-070: log Nt-form release fires whose target handle matches. + // Logged regardless of success/limit-exceeded — distinguished by + // result/previous_count in subsequent analysis. + audit_70::check_release(static_cast(sem_handle), + "NtReleaseSemaphore", + static_cast(release_count), + previous_count); } else { result = X_STATUS_INVALID_HANDLE; } @@ -954,6 +988,19 @@ uint32_t xeKeWaitForSingleObject(void* object_ptr, uint32_t wait_reason, return X_STATUS_ABANDONED_WAIT_0; } + // Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's + // `ke_wait_for_single_object`. Resolve the SID via the object's + // first registered handle. + if (phase_a::IsEnabled()) { + uint64_t sid = 0; + if (!object->handles().empty()) { + sid = phase_a::LookupHandleSemanticId(object->handles()[0]); + } + int64_t timeout_ns = timeout_ptr ? (static_cast(*timeout_ptr) * 100) : -1; + phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0, + /* wait_all */ false); + } + X_STATUS result = object->Wait(wait_reason, processor_mode, alertable, timeout_ptr); if (alertable) { @@ -980,6 +1027,16 @@ uint32_t NtWaitForSingleObjectEx(uint32_t object_handle, uint32_t wait_mode, uint32_t alertable, uint64_t* timeout_ptr) { X_STATUS result = X_STATUS_SUCCESS; + // Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's + // `nt_wait_for_single_object_ex`. Resolve SID directly from the + // handle. + if (phase_a::IsEnabled()) { + uint64_t sid = phase_a::LookupHandleSemanticId(object_handle); + int64_t timeout_ns = timeout_ptr ? (static_cast(*timeout_ptr) * 100) : -1; + phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0, + /* wait_all */ false); + } + auto object = kernel_state()->object_table()->LookupObject(object_handle); if (object) {