From 30d05ee97b6eb93943bdf0ce9d6a80d9332fec72 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Tue, 28 Jul 2026 20:03:33 +0000 Subject: [PATCH] snapshot: preserve the uncommitted Canary instrumentation (not authored here) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Working-tree snapshot taken 2026-07-28 so this work is not lost. To be clear about provenance: NONE of this was written in the session that committed it — it is pre-existing uncommitted work on phase-a-tracing, last committed 2026-07-09, from the xenia-rs decoder/deadlock investigation. It is recorded verbatim, not reviewed and not tested. Contents: 24 tracked modifications (ppc_emit_memory, xex_module, object_table, shim_utils, xboxkrnl threading/rtl, xevent/xobject/xthread, memory) plus the untracked probe sources audit_68_host_mem_watch, audit_69_event_signal_watch, audit_70_semaphore_release_watch, phase_b_snapshot, and cmake/toolchains. Deliberately excluded: build-cross/ (51 GB of build output) and vkd3d-proton.cache. Note third_party/snappy is a submodule pointer change whose target may not be pushed anywhere, so this branch is a preservation record rather than a guaranteed-buildable tree. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 27 +- CMakePresets.json | 31 + cmake/XeniaHelpers.cmake | 8 +- cmake/toolchains/linux-to-win-msvc.cmake | 119 +++ cmake/toolchains/xwin-case-symlinks.py | 126 +++ src/xenia/app/main_resources.rc | 4 +- .../base/audit_68_host_mem_watch_base.cc | 455 +++++++++ src/xenia/base/audit_68_host_mem_watch_fwd.h | 95 ++ src/xenia/base/byte_order.h | 26 + src/xenia/base/mapped_memory_win.cc | 6 +- src/xenia/base/memory.h | 37 + src/xenia/cpu/ppc/ppc_emit_altivec.cc | 37 +- src/xenia/cpu/ppc/ppc_emit_memory.cc | 178 +++- src/xenia/cpu/xex_module.cc | 52 + src/xenia/emulator.cc | 11 + .../kernel/audit_69_event_signal_watch.cc | 193 ++++ .../kernel/audit_69_event_signal_watch.h | 51 + .../audit_70_semaphore_release_watch.cc | 183 ++++ .../kernel/audit_70_semaphore_release_watch.h | 52 + src/xenia/kernel/phase_b_snapshot.cc | 922 ++++++++++++++++++ src/xenia/kernel/phase_b_snapshot.h | 43 + src/xenia/kernel/util/object_table.cc | 55 ++ src/xenia/kernel/util/shim_utils.h | 52 + src/xenia/kernel/xboxkrnl/xboxkrnl_rtl.cc | 8 + .../kernel/xboxkrnl/xboxkrnl_threading.cc | 57 ++ src/xenia/kernel/xevent.cc | 8 + src/xenia/kernel/xobject.cc | 27 + src/xenia/kernel/xthread.cc | 20 + src/xenia/kernel/xthread.h | 5 + src/xenia/memory.cc | 89 ++ src/xenia/memory.h | 7 + third_party/CMakeLists.txt | 9 +- third_party/snappy | 2 +- tools/build/compile_shader_dxbc.py | 18 +- 34 files changed, 2981 insertions(+), 32 deletions(-) create mode 100644 cmake/toolchains/linux-to-win-msvc.cmake create mode 100644 cmake/toolchains/xwin-case-symlinks.py create mode 100644 src/xenia/base/audit_68_host_mem_watch_base.cc create mode 100644 src/xenia/base/audit_68_host_mem_watch_fwd.h create mode 100644 src/xenia/kernel/audit_69_event_signal_watch.cc create mode 100644 src/xenia/kernel/audit_69_event_signal_watch.h create mode 100644 src/xenia/kernel/audit_70_semaphore_release_watch.cc create mode 100644 src/xenia/kernel/audit_70_semaphore_release_watch.h create mode 100644 src/xenia/kernel/phase_b_snapshot.cc create mode 100644 src/xenia/kernel/phase_b_snapshot.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 92f0bd625..3f8f13534 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,20 @@ file(MAKE_DIRECTORY "${PROJECT_SOURCE_DIR}/scratch") # Python for shader compilation scripts find_package(Python3 REQUIRED COMPONENTS Interpreter) +# Generate build-tree version.h via xenia-build.py's generate_version_h() +# (normally invoked by `xb premake`/`xb build`; CMake-direct flows need it too). +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import importlib.util,sys; spec=importlib.util.spec_from_file_location('xb',r'${PROJECT_SOURCE_DIR}/xenia-build.py'); m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m); m.generate_version_h(r'${CMAKE_BINARY_DIR}')" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE _xenia_version_rc +) +if(NOT _xenia_version_rc EQUAL 0) + message(WARNING "version.h generation failed (rc=${_xenia_version_rc}); writing stub.") + file(WRITE "${CMAKE_BINARY_DIR}/version.h" + "#ifndef GENERATED_VERSION_H_\n#define GENERATED_VERSION_H_\n#define XE_BUILD_BRANCH \"unknown\"\n#define XE_BUILD_COMMIT \"unknown\"\n#define XE_BUILD_COMMIT_SHORT \"unknown\"\n#define XE_BUILD_DATE __DATE__\n#endif\n" + ) +endif() + # Include helpers include(cmake/XeniaHelpers.cmake) @@ -142,8 +156,17 @@ if(MSVC) # --- Per-configuration MSVC flags --- # Checked - string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address") - string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address") + # /RTCsu is MSVC-only; clang-cl ignores it with a per-TU warning. Gate it + # so the Checked config stays quiet when cross-compiling with clang-cl. + # ASan (/fsanitize=address) stays active in both branches. + if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" + AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address") + string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address") + else() + string(APPEND CMAKE_C_FLAGS_CHECKED " /fsanitize=address") + string(APPEND CMAKE_CXX_FLAGS_CHECKED " /fsanitize=address") + endif() string(APPEND CMAKE_EXE_LINKER_FLAGS_CHECKED " /INCREMENTAL:NO") add_compile_definitions($<$:DEBUG>) diff --git a/CMakePresets.json b/CMakePresets.json index e05265822..3bb0b979f 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -39,6 +39,22 @@ "cacheVariables": { "CMAKE_SYSTEM_PROCESSOR": "ARM64" } + }, + { + "name": "cross-win-clangcl", + "displayName": "Cross (Linux->Win MSVC) clang-cl + xwin", + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build-cross", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-to-win-msvc.cmake", + "condition": { + "type": "notEquals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "XWIN_DIR": "$env{HOME}/.xwin/splat", + "FXC_PATH": "$env{HOME}/.local/share/xenia-cross/fxc/fxc.exe" + } } ], "buildPresets": [ @@ -89,6 +105,21 @@ "name": "vs-arm64-checked", "configurePreset": "vs-arm64", "configuration": "Checked" + }, + { + "name": "cross-debug", + "configurePreset": "cross-win-clangcl", + "configuration": "Debug" + }, + { + "name": "cross-release", + "configurePreset": "cross-win-clangcl", + "configuration": "Release" + }, + { + "name": "cross-checked", + "configurePreset": "cross-win-clangcl", + "configuration": "Checked" } ] } diff --git a/cmake/XeniaHelpers.cmake b/cmake/XeniaHelpers.cmake index b712553ca..f8e94d016 100644 --- a/cmake/XeniaHelpers.cmake +++ b/cmake/XeniaHelpers.cmake @@ -192,6 +192,12 @@ function(xe_shader_rules_dxbc target shader_dir) set(_valid_stages vs hs ds gs ps cs) set(_commands) set(_bytecode_dir "${shader_dir}/bytecode/d3d12_5_1") + # Propagate FXC_PATH from the configure-time env so ninja-spawned python + # subprocesses can find fxc.exe (CMake `set(ENV{...})` doesn't reach build). + set(_env_prefix "") + if(DEFINED ENV{FXC_PATH}) + set(_env_prefix ${CMAKE_COMMAND} -E env "FXC_PATH=$ENV{FXC_PATH}") + endif() list(APPEND _commands COMMAND ${CMAKE_COMMAND} -E make_directory "${_bytecode_dir}") foreach(src ${_sources}) get_filename_component(_name ${src} NAME) @@ -206,7 +212,7 @@ function(xe_shader_rules_dxbc target shader_dir) if(NOT _stage IN_LIST _valid_stages) continue() endif() - list(APPEND _commands COMMAND ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h") + list(APPEND _commands COMMAND ${_env_prefix} ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h") endforeach() add_custom_command( OUTPUT "${_stamp}" diff --git a/cmake/toolchains/linux-to-win-msvc.cmake b/cmake/toolchains/linux-to-win-msvc.cmake new file mode 100644 index 000000000..5860cd0ff --- /dev/null +++ b/cmake/toolchains/linux-to-win-msvc.cmake @@ -0,0 +1,119 @@ +# Linux host -> Windows MSVC-ABI cross toolchain using clang-cl + lld-link +# + xwin-supplied Win10 SDK/CRT. Driven by Ninja Multi-Config. + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +if(NOT DEFINED XWIN_DIR) + if(DEFINED ENV{XWIN_DIR}) + set(XWIN_DIR "$ENV{XWIN_DIR}") + else() + set(XWIN_DIR "$ENV{HOME}/.xwin/splat") + endif() +endif() +set(XWIN_DIR "${XWIN_DIR}" CACHE PATH "xwin splat root (contains crt/ and sdk/)") + +if(NOT EXISTS "${XWIN_DIR}/crt/include") + message(FATAL_ERROR "XWIN_DIR=${XWIN_DIR} missing crt/include - run xwin splat.") +endif() + +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_LINKER lld-link) +set(CMAKE_RC_COMPILER llvm-rc) +set(CMAKE_AR llvm-lib) +set(CMAKE_MT llvm-mt) + +set(CMAKE_C_COMPILER_TARGET x86_64-pc-windows-msvc) +set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-windows-msvc) + +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# Force /MD (release CRT) for every config. xenia's CMakeLists.txt does a +# `string(REPLACE "/MDd" "/MD" ...)` on CMAKE_CXX_FLAGS_DEBUG, but with +# clang-cl, the runtime selection comes from CMAKE_MSVC_RUNTIME_LIBRARY +# (which expands to -MDd in dash form), so the substitution misses. +# Pinning the policy here avoids the need for non-redistributable debug +# CRT DLLs (MSVCP140D.dll etc) at runtime, which xwin doesn't ship. +cmake_policy(SET CMP0091 NEW) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") + +# xwin's default splat is a flat layout (crt/include, sdk/include/{ucrt,um,shared,...}), +# which clang-cl's /winsysroot does NOT understand. Use explicit -imsvc + -libpath: +# instead. Re-running `xwin splat --use-winsysroot-style` would also work but +# requires re-downloading ~600 MB. +# Use SHELL: prefix so CMake doesn't deduplicate repeated -imsvc tokens. +add_compile_options( + "SHELL:-imsvc \"${XWIN_DIR}/crt/include\"" + "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/ucrt\"" + "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/um\"" + "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/shared\"" + "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/winrt\"" +) +add_link_options( + "/libpath:${XWIN_DIR}/crt/lib/x86_64" + "/libpath:${XWIN_DIR}/sdk/lib/ucrt/x86_64" + "/libpath:${XWIN_DIR}/sdk/lib/um/x86_64" +) +# xwin pulls the latest MSVC STL which now hard-asserts Clang >= 19. Our host +# has Clang 18, which works fine in practice — opt out of the version check. +# (See yvals_core.h STL1000 in $XWIN_DIR/crt/include.) +add_compile_definitions(_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH) +# llvm-rc needs SDK headers explicitly + the resource file's own dir so the +# RC's relative ICON path (..\..\..\assets\icon\icon.ico) resolves. +get_filename_component(_toolchain_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +get_filename_component(_xenia_root "${_toolchain_dir}/../.." ABSOLUTE) +set(CMAKE_RC_FLAGS_INIT + "/I \"${XWIN_DIR}/sdk/include/um\" /I \"${XWIN_DIR}/sdk/include/shared\" /I \"${_xenia_root}/src/xenia/app\"") + +# Quiet MSVC-STL false positives and xenia-specific warnings that clang-cl +# emits but cl.exe doesn't (treated as errors under /WX). +add_compile_options( + -Wno-microsoft-include + -Wno-unused-command-line-argument + -Wno-ignored-pragma-intrinsic + -Wno-nonportable-include-path + -Wno-pragma-pack + -Wno-tautological-pointer-compare + -Wno-microsoft-cast + -Wno-deprecated-declarations + # These are silenced for native Linux Clang in CMakeLists.txt's else() branch, + # but the if(MSVC) branch fires under clang-cl and skips them — so re-add. + -Wno-switch + -Wno-attributes + -Wno-deprecated-register + -Wno-deprecated-volatile + -Wno-deprecated-enum-enum-conversion + # cl.exe accepts __pragma(optimize("s",on)); clang-cl only knows the empty + # argument form. xenia gates XE_MSVC_OPTIMIZE_SMALL on _MSC_VER so the + # rejected pragma still emits from the clang-cl path. Treat as no-op. + -Wno-ignored-pragmas + # xenia decorates several `virtual` methods with XE_FORCEINLINE; clang-cl + # then complains that the inline body isn't visible in includer TUs + # (definitions live in command_processor.cc). cl.exe accepts this silently. + -Wno-undefined-inline + -Wno-sizeof-pointer-memaccess + # `'ZM'` (PE magic, stored little-endian as "MZ" in the binary) — cl.exe + # accepts the multi-char constant silently; clang-cl errors under /WX. + -Wno-multichar +) +# _mm_cvtsi64x_si128 is an MSVC-only alias for the standard _mm_cvtsi64_si128. +# Used (gated on XE_PLATFORM_WIN32) in xenia/gpu/draw_util.cc. +add_compile_definitions(_mm_cvtsi64x_si128=_mm_cvtsi64_si128) + +# Plumb FXC for tools/build/compile_shader_dxbc.py (wine fxc auto-prepend). +# Use real Win10 SDK fxc.exe 10.x (supports SM 5_1, produces vkd3d-proton- +# acceptable DXBC). Extracted from the Win11 SDK 26100 ISO -> Store Apps +# Tools MSI's CABs. +if(DEFINED ENV{FXC_PATH}) + set(ENV_FXC "$ENV{FXC_PATH}") +else() + set(ENV_FXC "$ENV{HOME}/.local/share/xenia-cross/fxc/fxc.exe") +endif() +set(ENV{FXC_PATH} "${ENV_FXC}") + +set(CMAKE_FIND_ROOT_PATH "${XWIN_DIR}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/toolchains/xwin-case-symlinks.py b/cmake/toolchains/xwin-case-symlinks.py new file mode 100644 index 000000000..5ccf7915e --- /dev/null +++ b/cmake/toolchains/xwin-case-symlinks.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Scan xenia source for #include directives and ensure case-aliases exist +in xwin's flat SDK/CRT include dirs. xwin's default splat adds lowercase +aliases for headers that ship with uppercase names (e.g. Windows.h), +but does NOT add aliases in the reverse direction. xenia (and its +third_party deps) include some headers using historical Microsoft casing +(, ) that don't match the lowercase filenames the +SDK ships with. This helper creates the missing symlinks. + +Idempotent: re-running is safe. +""" +from __future__ import annotations +import os +import re +import sys +from pathlib import Path + +INCLUDE_RE = re.compile(r'^\s*#\s*include\s*<([A-Za-z][A-Za-z0-9_/.\-]*\.h)>', + re.MULTILINE) + +SOURCE_ROOTS = [ + "src", + "third_party/SDL2/src", + "third_party/SDL2/include", + "third_party/discord-rpc/src", + "third_party/fmt", + "third_party/imgui", +] + +SOURCE_EXTS = {".h", ".hpp", ".inc", ".c", ".cc", ".cpp", ".cxx"} + + +def collect_includes(repo_root: Path) -> set[str]: + """Return set of all -style include targets found in source.""" + seen: set[str] = set() + for root in SOURCE_ROOTS: + base = repo_root / root + if not base.exists(): + continue + for path in base.rglob("*"): + if path.suffix.lower() not in SOURCE_EXTS: + continue + try: + txt = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + for m in INCLUDE_RE.finditer(txt): + seen.add(m.group(1)) + return seen + + +def index_dir(d: Path) -> dict[str, str]: + """Return mapping lowercased-basename -> actual basename for files in d.""" + if not d.is_dir(): + return {} + idx: dict[str, str] = {} + for entry in d.iterdir(): + idx.setdefault(entry.name.lower(), entry.name) + return idx + + +def fix_dir(target_dir: Path, want: set[str]) -> tuple[int, int, list[str]]: + """For each header name in `want` (basename only), create a symlink to + the case-folded sibling if the exact case doesn't exist. Returns + (created, already_ok, missing-from-sdk).""" + if not target_dir.is_dir(): + return 0, 0, [] + idx = index_dir(target_dir) + created = 0 + already_ok = 0 + missing: list[str] = [] + for name in want: + # Only look at single-segment names; subdir/foo.h handled separately + if "/" in name: + continue + actual = idx.get(name.lower()) + if actual is None: + missing.append(name) + continue + if actual == name: + already_ok += 1 + continue + link = target_dir / name + if link.exists() or link.is_symlink(): + already_ok += 1 + continue + try: + link.symlink_to(actual) + created += 1 + except OSError as e: + print(f" ! failed to symlink {link} -> {actual}: {e}", + file=sys.stderr) + return created, already_ok, missing + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: xwin-case-symlinks.py ", + file=sys.stderr) + return 2 + repo_root = Path(sys.argv[1]).resolve() + xwin_root = Path(sys.argv[2]).resolve() + if not (xwin_root / "crt" / "include").exists(): + print(f"error: {xwin_root}/crt/include missing", file=sys.stderr) + return 1 + want = collect_includes(repo_root) + print(f"Collected {len(want)} unique #include <...> targets") + sdk_dirs = [ + xwin_root / "crt" / "include", + xwin_root / "sdk" / "include" / "ucrt", + xwin_root / "sdk" / "include" / "um", + xwin_root / "sdk" / "include" / "shared", + xwin_root / "sdk" / "include" / "winrt", + ] + total_created = 0 + for d in sdk_dirs: + created, ok, _missing = fix_dir(d, want) + if created: + print(f" {d}: created {created} case-symlinks") + total_created += created + print(f"Done: created {total_created} case-symlinks total") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/xenia/app/main_resources.rc b/src/xenia/app/main_resources.rc index bcc22d20f..94d8dda42 100644 --- a/src/xenia/app/main_resources.rc +++ b/src/xenia/app/main_resources.rc @@ -1,3 +1,5 @@ //{{NO_DEPENDENCIES}} -MAINICON ICON "..\\..\\..\\assets\\icon\\icon.ico" +// Forward slashes: portable across rc.exe and llvm-rc (which on Linux +// treats backslashes as literal filename chars, breaking path resolution). +MAINICON ICON "../../../assets/icon/icon.ico" diff --git a/src/xenia/base/audit_68_host_mem_watch_base.cc b/src/xenia/base/audit_68_host_mem_watch_base.cc new file mode 100644 index 000000000..e8b86fcd5 --- /dev/null +++ b/src/xenia/base/audit_68_host_mem_watch_base.cc @@ -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 + * (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 +#include +#include +#include +#include +#include +#include +#include + +#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 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 g_values; +std::vector 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::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(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(value) == v) return true; + if (size == 8 && static_cast(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(host_ptr), value, + static_cast(size), tag ? tag : "", + 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 g_read_probes; +std::atomic g_read_probe_thread_running{false}; +std::atomic 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(std::stoul(sva, nullptr, 0)); + uint32_t sz = static_cast(std::stoul(ssz, nullptr, 0)); + if (sz != 1 && sz != 2 && sz != 4 && sz != 8) return false; + out->size = static_cast(sz); + out->period_ns = static_cast(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(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 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(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(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(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(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(rp.size), + now_ns); + rp.last_was_valid = false; + } + next_fire[i] = static_cast(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::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 diff --git a/src/xenia/base/audit_68_host_mem_watch_fwd.h b/src/xenia/base/audit_68_host_mem_watch_fwd.h new file mode 100644 index 000000000..2714dc136 --- /dev/null +++ b/src/xenia/base/audit_68_host_mem_watch_fwd.h @@ -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) + * 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 +#include + +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 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"). 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_ diff --git a/src/xenia/base/byte_order.h b/src/xenia/base/byte_order.h index 5a076f319..c80ee0ffc 100644 --- a/src/xenia/base/byte_order.h +++ b/src/xenia/base/byte_order.h @@ -11,6 +11,7 @@ #define XENIA_BASE_BYTE_ORDER_H_ #include +#include #if defined __has_include #if __has_include() #include @@ -21,6 +22,7 @@ #endif #include "xenia/base/assert.h" +#include "xenia/base/audit_68_host_mem_watch_fwd.h" #include "xenia/base/platform.h" #if !__cpp_lib_endian @@ -88,6 +90,30 @@ struct endian_store { operator T() const { return get(); } void set(const T& src) { + // AUDIT-068 Session 2: hook the canonical be/le write path. Gated + // on the host→guest thunk being installed by Memory::Memory(); without + // that there is no Memory and therefore no possible guest-memory write. + // This ALSO prevents the slow-path from running during static-init order + // (which would race the cvar object construction in cpu_flags.cc and + // permanently latch g_active=0 before --audit_68_* cmdline override + // applies). See reading-error #35 / Session 2 plan. + if constexpr (sizeof(T) <= 8 && std::is_integral_v) { + if (xe::audit_68::g_host_to_guest_thunk != nullptr) [[unlikely]] { + uint64_t v; + if constexpr (sizeof(T) == 8) { + v = static_cast(src); + } else if constexpr (sizeof(T) == 4) { + v = static_cast(static_cast(src)); + } else if constexpr (sizeof(T) == 2) { + v = static_cast(static_cast(src)); + } else { + v = static_cast(static_cast(src)); + } + xe::audit_68::check_host_write( + &value, v, static_cast(sizeof(T)), + E == std::endian::big ? "be::set" : "le::set"); + } + } if constexpr (std::endian::native == E) { value = src; } else { diff --git a/src/xenia/base/mapped_memory_win.cc b/src/xenia/base/mapped_memory_win.cc index 9c1e52bdb..0f57c9cec 100644 --- a/src/xenia/base/mapped_memory_win.cc +++ b/src/xenia/base/mapped_memory_win.cc @@ -26,8 +26,10 @@ namespace xe { class Win32MappedMemory : public MappedMemory { public: // CreateFile returns INVALID_HANDLE_VALUE in case of failure. - // chrispy: made inline const to get around clang error - static inline constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE; + // INVALID_HANDLE_VALUE expands to a reinterpret_cast which MSVC accepts in + // constexpr as an extension; clang-cl rejects it per the C++ standard. + // `const` works for all the equality/assignment uses below. + static inline const HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE; // CreateFileMapping returns nullptr in case of failure. static constexpr HANDLE kMappingHandleInvalid = nullptr; diff --git a/src/xenia/base/memory.h b/src/xenia/base/memory.h index 8ef40bbff..e78c8499c 100644 --- a/src/xenia/base/memory.h +++ b/src/xenia/base/memory.h @@ -18,6 +18,7 @@ #include #include +#include "xenia/base/audit_68_host_mem_watch_fwd.h" #include "xenia/base/byte_order.h" namespace xe { @@ -354,34 +355,52 @@ template void store(void* mem, const T& value); template <> inline void store(void* mem, const int8_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 1, "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint8_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 1, + "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int16_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 2, "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint16_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 2, + "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int32_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 4, "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint32_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 4, + "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int64_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 8, + "store"); *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint64_t& value) { + xe::audit_68::check_host_write(mem, value, 8, "store"); *reinterpret_cast(mem) = value; } template <> @@ -411,34 +430,52 @@ template void store_and_swap(void* mem, const T& value); template <> inline void store_and_swap(void* mem, const int8_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 1, "store_and_swap"); *reinterpret_cast(mem) = value; } template <> inline void store_and_swap(void* mem, const uint8_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 1, + "store_and_swap"); *reinterpret_cast(mem) = value; } template <> inline void store_and_swap(void* mem, const int16_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 2, "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint16_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 2, + "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const int32_t& value) { + xe::audit_68::check_host_write(mem, static_cast( + static_cast(value)), + 4, "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint32_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 4, + "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const int64_t& value) { + xe::audit_68::check_host_write(mem, static_cast(value), 8, + "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint64_t& value) { + xe::audit_68::check_host_write(mem, value, 8, "store_and_swap"); *reinterpret_cast(mem) = byte_swap(value); } template <> diff --git a/src/xenia/cpu/ppc/ppc_emit_altivec.cc b/src/xenia/cpu/ppc/ppc_emit_altivec.cc index 513b21391..c9af025ff 100644 --- a/src/xenia/cpu/ppc/ppc_emit_altivec.cc +++ b/src/xenia/cpu/ppc/ppc_emit_altivec.cc @@ -9,12 +9,28 @@ #include "xenia/cpu/ppc/ppc_emit-private.h" +#include #include "xenia/base/assert.h" +#include "xenia/cpu/cpu_flags.h" #include "xenia/cpu/ppc/ppc_context.h" #include "xenia/cpu/ppc/ppc_hir_builder.h" #include +// AUDIT-067: forward-decls. Defined in ppc_emit_memory.cc / ppc_hir_builder.cc. +namespace xe { +namespace cpu { +namespace audit67 { +const std::vector& vals(); +} +namespace ppc { +void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc, + ::xe::cpu::hir::Value* vec128, + ::xe::cpu::hir::Value* ea); +} +} +} + namespace xe { namespace cpu { namespace ppc { @@ -175,6 +191,21 @@ int InstrEmit_stvewx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, f.Shr(f.And(f.Truncate(ea, INT8_TYPE), f.LoadConstantUint8(0xF)), 2); Value* v = f.Extract(f.LoadVR(vd), el, INT32_TYPE); f.Store(ea, f.ByteSwap(v)); + if (!::xe::cpu::audit67::vals().empty()) { + // For stvewx: only one lane is actually stored; piggyback on the scalar + // value-watch helper by emitting the equivalent of stw of v at ea. + Value* pc_hi64 = + f.LoadConstantUint64(static_cast(i.address) << 32); + Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE); + Value* packed = f.Or(pc_hi64, ea_lo64); + const auto& vals = ::xe::cpu::audit67::vals(); + for (size_t idx = 0; idx < vals.size(); ++idx) { + Value* cmp = f.CompareEQ(v, f.LoadConstantUint32(vals[idx])); + f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed); + f.ContextBarrier(); + f.TrapTrue(cmp, static_cast(250 + idx)); + } + } return 0; } int InstrEmit_stvewx(PPCHIRBuilder& f, const InstrData& i) { @@ -187,7 +218,11 @@ int InstrEmit_stvewx128(PPCHIRBuilder& f, const InstrData& i) { int InstrEmit_stvx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, uint32_t ra, uint32_t rb) { Value* ea = f.And(CalculateEA_0(f, ra, rb), f.LoadConstantUint64(~0xFull)); - f.Store(ea, f.ByteSwap(f.LoadVR(vd))); + Value* vec = f.LoadVR(vd); + f.Store(ea, f.ByteSwap(vec)); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatchVec(f, i.address, vec, ea); + } return 0; } int InstrEmit_stvx(PPCHIRBuilder& f, const InstrData& i) { diff --git a/src/xenia/cpu/ppc/ppc_emit_memory.cc b/src/xenia/cpu/ppc/ppc_emit_memory.cc index b4bdabb49..a6b44697d 100644 --- a/src/xenia/cpu/ppc/ppc_emit_memory.cc +++ b/src/xenia/cpu/ppc/ppc_emit_memory.cc @@ -10,11 +10,22 @@ #include "xenia/cpu/ppc/ppc_emit-private.h" #include +#include #include "xenia/base/assert.h" #include "xenia/base/cvar.h" +#include "xenia/cpu/cpu_flags.h" #include "xenia/cpu/ppc/ppc_context.h" #include "xenia/cpu/ppc/ppc_hir_builder.h" +// AUDIT-067: forward-decl of value-watch table (defined in ppc_hir_builder.cc). +namespace xe { +namespace cpu { +namespace audit67 { +const std::vector& vals(); +} // namespace audit67 +} // namespace cpu +} // namespace xe + DEFINE_bool( disable_prefetch_and_cachecontrol, true, "Disables translating ppc prefetch/cache flush instructions to host " @@ -67,6 +78,90 @@ void StoreEA(PPCHIRBuilder& f, uint32_t rt, Value* ea) { f.StoreGPR(rt, ea); } +// AUDIT-067: emit a runtime equality check on the 32-bit value-to-be-stored +// against each configured watch value. On match, store (pc, EA) packed into +// the PPCContext scratch field so the native trap handler can read them, +// then fire a trap with code (kTrapBase + idx). Done host-side as a +// build-time pc constant + a runtime EA truncate, packed as +// (pc << 32) | (ea & 0xFFFFFFFF) so the handler can decompose. +static void EmitAudit67ValueWatch(PPCHIRBuilder& f, uint32_t pc, Value* val32, + Value* ea) { + const auto& vals = ::xe::cpu::audit67::vals(); + if (vals.empty()) return; + // pc is known at JIT time → emit as constant; ea is runtime. + Value* pc_hi64 = f.LoadConstantUint64(static_cast(pc) << 32); + Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE); + Value* packed = f.Or(pc_hi64, ea_lo64); + for (size_t idx = 0; idx < vals.size(); ++idx) { + Value* cmp = f.CompareEQ(val32, f.LoadConstantUint32(vals[idx])); + f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed); + f.ContextBarrier(); + f.TrapTrue(cmp, static_cast(250 + idx)); + } +} + +// AUDIT-067 128-bit (vector) variant: checks each of the 4 32-bit lanes in a +// vector store. Used for stvx/stvxl/stvewx (memcpy-derived installs may use +// 128-bit vector stores). The matched lane is reflected in the dst by +// adding (lane * 4) so the handler can see exactly where in memory the +// value lands. Declared with external linkage so altivec.cc can call it. +void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc, + Value* vec128, Value* ea) { + const auto& vals = ::xe::cpu::audit67::vals(); + if (vals.empty()) return; + Value* pc_hi64 = f.LoadConstantUint64(static_cast(pc) << 32); + for (size_t idx = 0; idx < vals.size(); ++idx) { + Value* watch = f.LoadConstantUint32(vals[idx]); + for (uint8_t lane = 0; lane < 4; ++lane) { + Value* lane_val = f.Extract(vec128, lane, INT32_TYPE); + Value* cmp = f.CompareEQ(lane_val, watch); + Value* lane_off = f.LoadConstantUint32(static_cast(lane * 4)); + Value* dst32 = f.Add(f.Truncate(ea, INT32_TYPE), lane_off); + Value* packed = f.Or(pc_hi64, f.ZeroExtend(dst32, INT64_TYPE)); + f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed); + f.ContextBarrier(); + f.TrapTrue(cmp, static_cast(250 + idx)); + } + } +} + +// AUDIT-067 64-bit variant: same as above but checks BOTH halves of a 64-bit +// stored value. EA points at the start of the 8-byte store; the matched half +// is encoded into the trap idx via (250 + 2*idx + half), where half=0 means +// upper 32 bits (lower address), half=1 means lower 32 bits (upper address). +static void EmitAudit67ValueWatch64(PPCHIRBuilder& f, uint32_t pc, Value* val64, + Value* ea) { + const auto& vals = ::xe::cpu::audit67::vals(); + if (vals.empty()) return; + // PowerPC is big-endian: u64 stored at EA places upper-32 bits at EA+0 + // and lower-32 bits at EA+4. Check both halves against each watch value. + Value* upper32 = f.Truncate(f.Shr(val64, int8_t(32)), INT32_TYPE); // bits[63:32] + Value* lower32 = f.Truncate(val64, INT32_TYPE); // bits[31:0] + Value* pc_hi64 = f.LoadConstantUint64(static_cast(pc) << 32); + for (size_t idx = 0; idx < vals.size(); ++idx) { + // Upper half lands at EA+0. + { + Value* cmp = f.CompareEQ(upper32, f.LoadConstantUint32(vals[idx])); + Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE); + Value* packed = f.Or(pc_hi64, ea_lo64); + f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed); + f.ContextBarrier(); + f.TrapTrue(cmp, static_cast(250 + idx)); + } + // Lower half lands at EA+4. + { + Value* cmp = f.CompareEQ(lower32, f.LoadConstantUint32(vals[idx])); + Value* ea_plus4 = + f.Add(f.Truncate(ea, INT32_TYPE), f.LoadConstantUint32(4)); + Value* ea_lo64 = f.ZeroExtend(ea_plus4, INT64_TYPE); + Value* packed = f.Or(pc_hi64, ea_lo64); + f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed); + f.ContextBarrier(); + f.TrapTrue(cmp, static_cast(250 + idx)); + } + } +} + // Integer load (A-13) int InstrEmit_lbz(PPCHIRBuilder& f, const InstrData& i) { @@ -518,9 +613,11 @@ int InstrEmit_stw(PPCHIRBuilder& f, const InstrData& i) { b = f.LoadGPR(i.D.RA); } Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS)); - f.StoreOffset(b, offset, - f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE))); - + Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE); + f.StoreOffset(b, offset, f.ByteSwap(val32)); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset)); + } return 0; } @@ -532,10 +629,14 @@ int InstrEmit_stmw(PPCHIRBuilder& f, const InstrData& i) { b = f.LoadGPR(i.D.RA); } + const bool watch_active = !::xe::cpu::audit67::vals().empty(); for (uint32_t j = 0; j < 32 - i.D.RT; ++j) { Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS) + j * 4); - f.StoreOffset(b, offset, - f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE))); + Value* val32 = f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE); + f.StoreOffset(b, offset, f.ByteSwap(val32)); + if (watch_active) { + EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset)); + } } return 0; } @@ -545,8 +646,12 @@ int InstrEmit_stwu(PPCHIRBuilder& f, const InstrData& i) { // MEM(EA, 4) <- (RS)[32:63] // RA <- EA Value* ea = CalculateEA_i(f, i.D.RA, XEEXTS16(i.D.DS)); - f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE))); + Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE); + f.Store(ea, f.ByteSwap(val32)); StoreEA(f, i.D.RA, ea); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, ea); + } return 0; } @@ -555,8 +660,12 @@ int InstrEmit_stwux(PPCHIRBuilder& f, const InstrData& i) { // MEM(EA, 4) <- (RS)[32:63] // RA <- EA Value* ea = CalculateEA(f, i.X.RA, i.X.RB); - f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE))); + Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE); + f.Store(ea, f.ByteSwap(val32)); StoreEA(f, i.X.RA, ea); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, ea); + } return 0; } @@ -568,7 +677,11 @@ int InstrEmit_stwx(PPCHIRBuilder& f, const InstrData& i) { // EA <- b + (RB) // MEM(EA, 4) <- (RS)[32:63] Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE))); + Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE); + f.Store(ea, f.ByteSwap(val32)); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, ea); + } return 0; } @@ -587,7 +700,11 @@ int InstrEmit_std(PPCHIRBuilder& f, const InstrData& i) { } Value* offset = f.LoadConstantInt64(XEEXTS16(i.DS.DS << 2)); - f.StoreOffset(b, offset, f.ByteSwap(f.LoadGPR(i.DS.RT))); + Value* val64 = f.LoadGPR(i.DS.RT); + f.StoreOffset(b, offset, f.ByteSwap(val64)); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, f.Add(b, offset)); + } return 0; } @@ -596,8 +713,12 @@ int InstrEmit_stdu(PPCHIRBuilder& f, const InstrData& i) { // MEM(EA, 8) <- (RS) // RA <- EA Value* ea = CalculateEA_i(f, i.DS.RA, XEEXTS16(i.DS.DS << 2)); - f.Store(ea, f.ByteSwap(f.LoadGPR(i.DS.RT))); + Value* val64 = f.LoadGPR(i.DS.RT); + f.Store(ea, f.ByteSwap(val64)); StoreEA(f, i.DS.RA, ea); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, ea); + } return 0; } @@ -606,8 +727,12 @@ int InstrEmit_stdux(PPCHIRBuilder& f, const InstrData& i) { // MEM(EA, 8) <- (RS) // RA <- EA Value* ea = CalculateEA(f, i.X.RA, i.X.RB); - f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT))); + Value* val64 = f.LoadGPR(i.X.RT); + f.Store(ea, f.ByteSwap(val64)); StoreEA(f, i.X.RA, ea); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, ea); + } return 0; } @@ -619,7 +744,11 @@ int InstrEmit_stdx(PPCHIRBuilder& f, const InstrData& i) { // EA <- b + (RB) // MEM(EA, 8) <- (RS) Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT))); + Value* val64 = f.LoadGPR(i.X.RT); + f.Store(ea, f.ByteSwap(val64)); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, ea); + } return 0; } @@ -684,7 +813,11 @@ int InstrEmit_stwbrx(PPCHIRBuilder& f, const InstrData& i) { // EA <- b + (RB) // MEM(EA, 4) <- bswap((RS)[32:63]) Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - f.Store(ea, f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)); + Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE); + f.Store(ea, val32); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, ea); + } return 0; } @@ -696,7 +829,11 @@ int InstrEmit_stdbrx(PPCHIRBuilder& f, const InstrData& i) { // EA <- b + (RB) // MEM(EA, 8) <- bswap(RS) Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - f.Store(ea, f.LoadGPR(i.X.RT)); + Value* val64 = f.LoadGPR(i.X.RT); + f.Store(ea, val64); + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, ea); + } return 0; } @@ -843,7 +980,8 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) { // This will always succeed if under the global lock, however. Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - Value* rt = f.ByteSwap(f.LoadGPR(i.X.RT)); + Value* val64 = f.LoadGPR(i.X.RT); + Value* rt = f.ByteSwap(val64); if (cvars::no_reserved_ops) { f.Store(ea, rt); @@ -862,6 +1000,9 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) { if (!cvars::no_reserved_ops) { f.MemoryBarrier(); } + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch64(f, i.address, val64, ea); + } return 0; } @@ -885,7 +1026,8 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) { Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB); - Value* rt = f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)); + Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE); + Value* rt = f.ByteSwap(val32); if (cvars::no_reserved_ops) { f.Store(ea, rt); @@ -904,7 +1046,9 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) { if (!cvars::no_reserved_ops) { f.MemoryBarrier(); } - + if (!::xe::cpu::audit67::vals().empty()) { + EmitAudit67ValueWatch(f, i.address, val32, ea); + } return 0; } // Floating-point load (A-19) diff --git a/src/xenia/cpu/xex_module.cc b/src/xenia/cpu/xex_module.cc index 1034dcac7..38148010c 100644 --- a/src/xenia/cpu/xex_module.cc +++ b/src/xenia/cpu/xex_module.cc @@ -51,6 +51,38 @@ DECLARE_bool(allow_plugins); DECLARE_bool(disable_context_promotion); +// AUDIT-068 Session 2: helper that scans a raw byte buffer for 4-byte aligned +// u32 values that match the configured audit_68 value list, emitting a +// per-position event. Used to pre-scan XEX-loader memcpys that bypass all +// other hooked surfaces. Cost when off: a single relaxed atomic load. +static inline void audit68_prescan_memcpy(uint32_t guest_va_dest, + const uint8_t* src, size_t size, + const char* tag) { + uint32_t active = xe::audit_68::g_active.load(std::memory_order_relaxed); + if (active == 0) return; + if ((active & 0x1) && size >= 4) { + size_t aligned_end = size & ~size_t(3); + for (size_t i = 0; i < aligned_end; i += 4) { + uint32_t be_u32 = (uint32_t(src[i + 0]) << 24) | + (uint32_t(src[i + 1]) << 16) | + (uint32_t(src[i + 2]) << 8) | uint32_t(src[i + 3]); + xe::audit_68::check_guest_va( + static_cast(guest_va_dest + i), be_u32, 4, tag); + } + } + if (active & 0x2) { + // Coarse addr-only event over the full span (dest only). + uint64_t v = 0; + if (size >= 4) { + v = (uint64_t(src[0]) << 24) | (uint64_t(src[1]) << 16) | + (uint64_t(src[2]) << 8) | uint64_t(src[3]); + } + xe::audit_68::check_guest_va(guest_va_dest, v, + static_cast(std::min(size, 8)), + tag); + } +} + static constexpr uint8_t xe_xex1_retail_key[16] = { 0xA2, 0x6C, 0x10, 0xF7, 0x1F, 0xD9, 0x35, 0xE9, 0x8B, 0x99, 0x92, 0x2C, 0xE9, 0x32, 0x15, 0x72}; @@ -424,6 +456,10 @@ int XexModule::ApplyPatch(XexModule* module) { // If image_source_offset is set, copy [source_offset:source_size] to // target_offset if (patch_header->delta_image_source_offset) { + audit68_prescan_memcpy( + module->base_address_ + patch_header->delta_image_target_offset, + base_exe + patch_header->delta_image_source_offset, + patch_header->delta_image_source_size, "xex_memcpy_patch"); memcpy(base_exe + patch_header->delta_image_target_offset, base_exe + patch_header->delta_image_source_offset, patch_header->delta_image_source_size); @@ -589,6 +625,8 @@ int XexModule::ReadImageUncompressed(const void* xex_addr, size_t xex_length) { if (exe_length > uncompressed_size) { return 1; } + audit68_prescan_memcpy(base_address_, p, exe_length, + "xex_memcpy_uncompressed"); memcpy(buffer, p, exe_length); return 0; case XEX_ENCRYPTION_NORMAL: @@ -665,6 +703,9 @@ int XexModule::ReadImageBasicCompressed(const void* xex_addr, // Overflow. return 1; } + audit68_prescan_memcpy( + base_address_ + static_cast(d - buffer), p, data_size, + "xex_memcpy_basic_block"); memcpy(d, p, data_size); break; case XEX_ENCRYPTION_NORMAL: { @@ -799,6 +840,17 @@ int XexModule::ReadImageCompressed(const void* xex_addr, size_t xex_length) { result_code = lzx_decompress( compress_buffer, d - compress_buffer, buffer, uncompressed_size, compression_info->normal.window_size, nullptr, 0); + + // AUDIT-068 Session 2: lzx_decompress writes directly into guest + // memory via the host pointer `buffer`. There's no host-side hook + // covering its internal bulk writes, so post-scan the produced bytes + // to recover what the XEX loader actually placed at `base_address_`. + // This is THE most likely catch for the vtable install case (vtables + // live in the .rdata section that is part of the LZX-compressed image). + if (result_code == 0) { + audit68_prescan_memcpy(base_address_, buffer, uncompressed_size, + "xex_lzx_decompress_output"); + } } else { XELOGE("Unable to allocate XEX memory at {:08X}-{:08X}.", base_address_, uncompressed_size); diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 4da09db80..09af5d1c0 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -217,6 +217,13 @@ X_STATUS Emulator::Setup( // logical processors. xe::threading::EnableAffinityConfiguration(); + if (cvars::audit_demo_setup_trace) { + XELOGI("AUDIT-DEMO-SETUP-BEGIN pid={:#x} guest_tick_hz={} time_scalar={}", + reinterpret_cast(this), + Clock::guest_tick_frequency(), + Clock::guest_time_scalar()); + } + XELOGI("{}: Initializing Memory...", __func__); // Create memory system first, as it is required for other systems. memory_ = std::make_unique(); @@ -278,6 +285,10 @@ X_STATUS Emulator::Setup( XELOGE("{}: Cannot initalize graphics_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; } + if (cvars::audit_demo_setup_trace) { + XELOGI("AUDIT-DEMO-SETUP-GRAPHICS-OK graphics_system={:p}", + static_cast(graphics_system_.get())); + } XELOGI("{}: Initializing HID...", __func__); // Initialize the HID. diff --git a/src/xenia/kernel/audit_69_event_signal_watch.cc b/src/xenia/kernel/audit_69_event_signal_watch.cc new file mode 100644 index 000000000..84d278f63 --- /dev/null +++ b/src/xenia/kernel/audit_69_event_signal_watch.cc @@ -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 +#include +#include +#include +#include +#include + +#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 g_active{0xFFFFFFFFu}; + +namespace { + +constexpr size_t kMaxEntries = 4; + +std::vector g_handles; +std::vector 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::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(v); + return true; +} + +void parse_csv_to_u32_vec(const std::string& csv, std::vector* 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(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 diff --git a/src/xenia/kernel/audit_69_event_signal_watch.h b/src/xenia/kernel/audit_69_event_signal_watch.h new file mode 100644 index 000000000..58b864916 --- /dev/null +++ b/src/xenia/kernel/audit_69_event_signal_watch.h @@ -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 +#include + +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 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_ diff --git a/src/xenia/kernel/audit_70_semaphore_release_watch.cc b/src/xenia/kernel/audit_70_semaphore_release_watch.cc new file mode 100644 index 000000000..eed9bd899 --- /dev/null +++ b/src/xenia/kernel/audit_70_semaphore_release_watch.cc @@ -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 +#include +#include +#include +#include +#include + +#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 g_active{0xFFFFFFFFu}; + +namespace { + +constexpr size_t kMaxEntries = 4; + +std::vector 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::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(v); + return true; +} + +void parse_csv_to_u32_vec(const std::string& csv, std::vector* 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(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(previous_count) + static_cast(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 diff --git a/src/xenia/kernel/audit_70_semaphore_release_watch.h b/src/xenia/kernel/audit_70_semaphore_release_watch.h new file mode 100644 index 000000000..879c50ec6 --- /dev/null +++ b/src/xenia/kernel/audit_70_semaphore_release_watch.h @@ -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 +#include + +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 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_ diff --git a/src/xenia/kernel/phase_b_snapshot.cc b/src/xenia/kernel/phase_b_snapshot.cc new file mode 100644 index 000000000..c4a3ac74b --- /dev/null +++ b/src/xenia/kernel/phase_b_snapshot.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 g_claimed{false}; +std::atomic 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(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(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 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(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 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 array_; + std::map obj_; + bool ordered_ = false; + std::vector 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(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 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 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 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 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 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& out_regions, + std::map& 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 all_regions; + std::map 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 heaps_arr; + for (uint32_t base : heap_probes) { + auto* heap = memory->LookupHeap(base); + if (!heap) continue; + std::map 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 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 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 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 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(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 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 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 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 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 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(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(t)); + root.Set("wall_clock_iso8601", JsonNode::String(wall)); + root.Set("host_ns_at_snapshot", JsonNode::Unsigned(0)); + + std::vector 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& 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 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 /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(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 diff --git a/src/xenia/kernel/phase_b_snapshot.h b/src/xenia/kernel/phase_b_snapshot.h new file mode 100644 index 000000000..a509e5a3f --- /dev/null +++ b/src/xenia/kernel/phase_b_snapshot.h @@ -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 + +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 /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_ diff --git a/src/xenia/kernel/util/object_table.cc b/src/xenia/kernel/util/object_table.cc index 2e29bb96b..3752a77b9 100644 --- a/src/xenia/kernel/util/object_table.cc +++ b/src/xenia/kernel/util/object_table.cc @@ -11,6 +11,7 @@ #include "xenia/base/byte_stream.h" #include "xenia/base/logging.h" +#include "xenia/kernel/event_log.h" #include "xenia/kernel/xobject.h" #include "xenia/kernel/xthread.h" @@ -18,6 +19,34 @@ namespace xe { namespace kernel { namespace util { +namespace { +// Phase C+15-α: map XObject::Type to schema-v1 object_type code. +// Schema codes are listed in event_log.h::ObjectType. Both engines +// must agree on this mapping (see ours's `KernelObject::schema_object_type`). +uint32_t SchemaObjectType(XObject::Type type) { + using T = XObject::Type; + switch (type) { + case T::Event: return phase_a::kObjEvent; + case T::Mutant: return phase_a::kObjMutant; + case T::Semaphore: return phase_a::kObjSemaphore; + case T::Timer: return phase_a::kObjTimer; + case T::Thread: return phase_a::kObjThread; + case T::File: return phase_a::kObjFile; + case T::IOCompletion: return phase_a::kObjIoCompletion; + case T::Module: return phase_a::kObjModule; + case T::Enumerator: return phase_a::kObjEnumState; + case T::NotifyListener: return phase_a::kObjNotification; + case T::SymbolicLink: return phase_a::kObjUnknown; + case T::Session: return phase_a::kObjUnknown; + case T::Socket: return phase_a::kObjUnknown; + case T::Device: return phase_a::kObjUnknown; + case T::Undefined: + default: + return phase_a::kObjUnknown; + } +} +} // namespace + ObjectTable::ObjectTable() {} ObjectTable::~ObjectTable() { Reset(); } @@ -154,6 +183,25 @@ X_STATUS ObjectTable::AddHandle(XObject* object, X_HANDLE* out_handle) { if (out_handle) { *out_handle = handle; } + // Phase C+15-α: schema-v1 `handle.create` event. Symmetric with + // ours's `KernelState::alloc_handle_for`. Cvar-gated default-off + // via `phase_a::IsEnabled()`. Centralized here so every object + // type (XEvent, XThread, XFile, etc.) emits a symmetric event + // when added to the object table. + // + // Phase C+18: skip the emit when called from inside + // `XObject::GetNativeObject` lazy-wrap. The shared-global + // `handle.create` is emitted explicitly by `GetNativeObject` + // with a scheduling-invariant SID via + // `phase_a::EmitHandleCreateSharedGlobal`. This avoids the + // race-prone per-thread SID for process-global dispatchers. + if (phase_a::IsEnabled() && !phase_a::IsInGetNativeObject()) { + uint32_t schema_type = SchemaObjectType(object->type()); + const std::string& name = object->name(); + phase_a::EmitHandleCreateAuto(/* create_site_pc */ 0, schema_type, + handle, + name.empty() ? nullptr : name.c_str()); + } } return result; @@ -239,6 +287,13 @@ X_STATUS ObjectTable::RemoveHandle(X_HANDLE handle) { if (!object->name().empty()) { RemoveNameMapping(object->name()); } + // Phase C+15-α: schema-v1 `handle.destroy` event. Symmetric with + // ours's `nt_close`. Emitted at RemoveHandle (which is reached + // when the final handle reference is released, matching ours's + // refcount==0 case). + if (phase_a::IsEnabled()) { + phase_a::EmitHandleDestroyAuto(handle, /* prior_refcount */ 1); + } // Release now that the object has been removed from the table. object->Release(); } diff --git a/src/xenia/kernel/util/shim_utils.h b/src/xenia/kernel/util/shim_utils.h index 0fa254157..a5efa40ce 100644 --- a/src/xenia/kernel/util/shim_utils.h +++ b/src/xenia/kernel/util/shim_utils.h @@ -499,6 +499,30 @@ enum class KernelModuleId { xbdm, }; +// Phase A bridge — see kernel/event_log.h. Inline to avoid pulling the +// header into shim_utils.h's transitive set. +namespace phase_a_bridge { +constexpr const char* KernelModuleIdName(KernelModuleId m) { + switch (m) { + case KernelModuleId::xboxkrnl: return "xboxkrnl.exe"; + case KernelModuleId::xam: return "xam.xex"; + case KernelModuleId::xbdm: return "xbdm.xex"; + } + return "unknown"; +} +bool Enabled(); +void EmitImportAndCall(const char* module_name, uint16_t ord, const char* name); +void EmitReturn(const char* name, uint64_t return_value); +// Phase C+10 schema-v1 extension: when `name` is in the known +// path-bearing export set, resolve the OBJECT_ATTRIBUTES* arg from the +// PPC context and emit a `kernel.call` event whose `args_resolved` +// field includes `{"path":"..."}`. Falls back to the empty form for +// unknown names. Bridge takes a `void*` to avoid pulling the PPCContext +// definition into this header's transitive include set. +void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord, + const char* name, void* ppc_context); +} // namespace phase_a_bridge + template requires(I == sizeof...(Ps)) void AppendKernelCallParams(StringBuffer& string_buffer, @@ -578,9 +602,18 @@ struct ExportRegistrerHelper { cvars::log_high_frequency_kernel_calls)) { PrintKernelCall(export_entry, params); } + const bool phase_a_on = phase_a_bridge::Enabled(); + if (phase_a_on) { + phase_a_bridge::EmitImportAndCallWithCtx( + phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL, + export_entry->name, ppc_context); + } if constexpr (std::is_void::value) { KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); + if (phase_a_on) { + phase_a_bridge::EmitReturn(export_entry->name, 0); + } } else { auto result = KernelTrampoline(fn, std::forward>(params), @@ -590,6 +623,11 @@ struct ExportRegistrerHelper { (xe::cpu::ExportTag::kLog | xe::cpu::ExportTag::kLogResult)) { // TODO(benvanik): log result. } + if (phase_a_on) { + phase_a_bridge::EmitReturn( + export_entry->name, + static_cast(ppc_context->r[3])); + } } } }; @@ -600,14 +638,28 @@ struct ExportRegistrerHelper { 0, }; std::tuple params = {Ps(init)...}; + const bool phase_a_on = phase_a_bridge::Enabled(); + if (phase_a_on) { + phase_a_bridge::EmitImportAndCallWithCtx( + phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL, + export_entry->name, ppc_context); + } if constexpr (std::is_void::value) { KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); + if (phase_a_on) { + phase_a_bridge::EmitReturn(export_entry->name, 0); + } } else { auto result = KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); result.Store(ppc_context); + if (phase_a_on) { + phase_a_bridge::EmitReturn( + export_entry->name, + static_cast(ppc_context->r[3])); + } } } }; diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_rtl.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_rtl.cc index 67bfa3322..ee9b7ed7c 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_rtl.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_rtl.cc @@ -13,6 +13,7 @@ #include "xenia/base/atomic.h" #include "xenia/base/pe_image.h" +#include "xenia/kernel/event_log.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" #include "xenia/kernel/util/shim_utils.h" @@ -620,6 +621,13 @@ void RtlEnterCriticalSection_entry(pointer_t cs) { } if (xe::atomic_inc(&cs->lock_count) != 0) { + // Phase D Stage 1: signal real contention before parking the host + // thread on the embedded dispatcher event. The Stage-3 manifest + // loader keys on this event's (tid, tid_event_idx) to force + // ours's `rtl_enter_critical_section` to park at the same per-tid + // ordinal. Cvar-gated (default off) inside the helper itself, so + // pre-Stage-1 cold runs are byte-identical. + phase_a::EmitContentionObserved(cs.guest_address(), /*contended=*/true); // Create a full waiter. xeKeWaitForSingleObject(reinterpret_cast(cs.host_address()), 8, 0, 0, nullptr); 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) { diff --git a/src/xenia/kernel/xevent.cc b/src/xenia/kernel/xevent.cc index b583bf732..f8bf47952 100644 --- a/src/xenia/kernel/xevent.cc +++ b/src/xenia/kernel/xevent.cc @@ -11,6 +11,7 @@ #include "xenia/base/byte_stream.h" #include "xenia/base/logging.h" +#include "xenia/kernel/audit_69_event_signal_watch.h" namespace xe { namespace kernel { @@ -58,12 +59,19 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) { } int32_t XEvent::Set(uint32_t priority_increment, bool wait) { + // AUDIT-069: log event-signal fires whose target matches the configured + // handle ID or native VA. Hot path is a single relaxed atomic load when + // the cvars are empty (default). + audit_69::check_event_set(this->handle(), this->guest_object(), + "XEvent::Set"); set_priority_increment(priority_increment); event_->Set(); return 1; } int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) { + audit_69::check_event_set(this->handle(), this->guest_object(), + "XEvent::Pulse"); set_priority_increment(priority_increment); event_->Pulse(); return 1; diff --git a/src/xenia/kernel/xobject.cc b/src/xenia/kernel/xobject.cc index 3c501509c..edfeead44 100644 --- a/src/xenia/kernel/xobject.cc +++ b/src/xenia/kernel/xobject.cc @@ -10,6 +10,7 @@ #include "xenia/kernel/xobject.h" #include "xenia/base/byte_stream.h" +#include "xenia/kernel/event_log.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" @@ -429,6 +430,15 @@ object_ref XObject::GetNativeObject(KernelState* kernel_state, // First use, create new. // https://www.nirsoft.net/kernel_struct/vista/KOBJECTS.html XObject* object = nullptr; + // Phase C+18: bracket the wrapper ctor + AddHandle path with a + // host-TLS flag so the centralized `ObjectTable::AddHandle` emit + // hook short-circuits its per-thread `handle.create`. We emit a + // shared-global `handle.create` explicitly below with a SID keyed + // on `(native_ptr, object_type)` — scheduling-invariant across + // engines/threads. See `event_log.h`/`event_log.cc` C+18 helpers + // and ours's `ensure_dispatcher_object`. + phase_a::SetInGetNativeObject(true); + uint32_t schema_object_type = phase_a::kObjUnknown; switch (as_type) { case 0: // EventNotificationObject case 1: // EventSynchronizationObject @@ -436,12 +446,14 @@ object_ref XObject::GetNativeObject(KernelState* kernel_state, auto ev = new XEvent(kernel_state); ev->InitializeNative(native_ptr, header); object = ev; + schema_object_type = phase_a::kObjEvent; } break; case 2: // MutantObject { auto mutant = new XMutant(kernel_state); mutant->InitializeNative(native_ptr, header); object = mutant; + schema_object_type = phase_a::kObjMutant; } break; case 5: // SemaphoreObject { @@ -450,6 +462,7 @@ object_ref XObject::GetNativeObject(KernelState* kernel_state, // Can't report failure to the guest at late initialization: assert_true(success); object = sem; + schema_object_type = phase_a::kObjSemaphore; } break; case 3: // ProcessObject case 4: // QueueObject @@ -468,10 +481,24 @@ object_ref XObject::GetNativeObject(KernelState* kernel_state, assert_always(); result = nullptr; } + phase_a::SetInGetNativeObject(false); // Stash pointer in struct. // FIXME: This assumes the object contains a dispatch header (some don't!) if (object) { StashHandle(header, object->handle()); + // Phase C+18: emit the shared-global `handle.create` here, AFTER + // StashHandle, with the deterministic SID. The `native_ptr` is + // already a guest VA (`X_DISPATCH_HEADER*`) so a host-pointer cast + // to uint32_t is meaningful in canary's memory model only when the + // value is the guest address — translate via the same memory base. + if (phase_a::IsEnabled()) { + uint32_t guest_ptr = + kernel_state->memory()->HostToGuestVirtual(native_ptr); + const std::string& name = object->name(); + phase_a::EmitHandleCreateSharedGlobal( + guest_ptr, schema_object_type, object->handle(), + name.empty() ? nullptr : name.c_str()); + } } result = object; } diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index cc7d90c2e..32ea77f5a 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -21,7 +21,9 @@ #include "xenia/base/threading.h" #include "xenia/cpu/processor.h" #include "xenia/emulator.h" +#include "xenia/kernel/event_log.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/phase_b_snapshot.h" #include "xenia/kernel/user_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" @@ -116,6 +118,11 @@ XThread* XThread::GetCurrentThread() { return thread; } +// Phase C+15-α: non-asserting variant. See xthread.h. +XThread* XThread::TryGetCurrentThread() { + return reinterpret_cast(current_xthread_tls_); +} + uint32_t XThread::GetCurrentThreadHandle() { XThread* thread = XThread::GetCurrentThread(); return thread->handle(); @@ -470,6 +477,14 @@ X_STATUS XThread::Create() { X_STATUS XThread::Exit(int exit_code) { // This may only be called on the thread itself. assert_true(XThread::GetCurrentThread() == this); + // Phase C+15-α: schema-v1 `thread.exit` event. Emit BEFORE Exit() + // unwinds via threading::Thread::Exit (does not return). This + // covers both `ExTerminateThread` and implicit thread-entry returns + // — XThread::Execute calls Exit() at line ~641 after entry_pc + // returns. Symmetric with ours's `ex_terminate_thread`. + if (phase_a::IsEnabled()) { + phase_a::EmitThreadExit(static_cast(exit_code)); + } // TODO(chrispy): not sure if this order is correct, should it come after // apcs? auto kthread = guest_object(); @@ -575,6 +590,11 @@ void XThread::Execute() { // On Windows, setjmp/longjmp is used because MSVC's longjmp performs SEH // stack unwinding which already calls destructors. uint32_t next_address; + // Phase B snapshot. No-op when phase_b_snapshot_dir cvar is empty + // (default). When set, fires once on the entry-point thread immediately + // before its first guest instruction executes. See + // xenia/kernel/phase_b_snapshot.h. + ::xe::kernel::phase_b::FireIfEntryThread(this, thread_state_, address); #if !XE_PLATFORM_WIN32 try { exit_code = static_cast(kernel_state()->processor()->Execute( diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index 527e549c5..427a59583 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -383,6 +383,11 @@ class XThread : public XObject, public cpu::Thread { static bool IsInThread(XThread* other); static bool IsInThread(); static XThread* GetCurrentThread(); + // Phase C+15-α: non-asserting accessor for hooks that may run from + // boot / host threads (Phase A event-log emit at ObjectTable::AddHandle). + // Returns nullptr instead of asserting when there is no current + // guest XThread. + static XThread* TryGetCurrentThread(); static uint32_t GetCurrentThreadHandle(); static uint32_t GetCurrentThreadId(); diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index 22ba66aee..f02b11d7f 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -14,6 +14,7 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/assert.h" +#include "xenia/base/audit_68_host_mem_watch_fwd.h" #include "xenia/base/byte_stream.h" #include "xenia/base/clock.h" #include "xenia/base/cvar.h" @@ -90,6 +91,9 @@ uint32_t get_page_count(uint32_t value, uint32_t page_size) { static Memory* active_memory_ = nullptr; +// AUDIT-068 — process-global accessor (declared in memory.h). +Memory* Memory::active() { return active_memory_; } + void CrashDump() { static std::atomic in_crash_dump(0); if (in_crash_dump.fetch_add(1)) { @@ -151,11 +155,41 @@ Memory::Memory() { uint32_t(xe::memory::allocation_granularity()); assert_zero(active_memory_); active_memory_ = this; + + // AUDIT-068: register host→guest translation thunk so the watch slow path + // in xenia-base can resolve guest VAs without depending on xenia-core. + xe::audit_68::g_host_to_guest_thunk = [](const void* host_ptr) -> uint32_t { + Memory* m = active_memory_; + return m ? m->HostToGuestVirtual(host_ptr) : 0u; + }; + + // AUDIT-068 Session 3: register guest→host translation thunk and a + // page-protect query thunk for the read-mode probe. The probe thread uses + // QueryProtect to skip unmapped/uncommitted pages before dereferencing. + xe::audit_68::g_guest_to_host_thunk = [](uint32_t va) -> const void* { + Memory* m = active_memory_; + return m ? reinterpret_cast(m->TranslateVirtual(va)) + : nullptr; + }; + xe::audit_68::g_query_protect_thunk = [](uint32_t va, + uint32_t* out_protect) -> bool { + Memory* m = active_memory_; + if (!m) return false; + BaseHeap* heap = m->LookupHeap(va); + if (!heap) { + if (out_protect) *out_protect = 0; + return false; + } + return heap->QueryProtect(va, out_protect); + }; } Memory::~Memory() { assert_true(active_memory_ == this); active_memory_ = nullptr; + xe::audit_68::g_host_to_guest_thunk = nullptr; + xe::audit_68::g_guest_to_host_thunk = nullptr; + xe::audit_68::g_query_protect_thunk = nullptr; // Uninstall the MMIO handler, as we won't be able to service more // requests. @@ -540,16 +574,71 @@ uint32_t Memory::GetPhysicalAddress(uint32_t address) const { } void Memory::Zero(uint32_t address, uint32_t size) { + // AUDIT-068: log a single span event with value=0; size is capped at 8 for + // the value field. Slow path is gated on the atomic flag. + xe::audit_68::check_guest_va(address, 0, + static_cast(std::min(size, 8)), + "Memory::Zero"); std::memset(TranslateVirtual(address), 0, size); } void Memory::Fill(uint32_t address, uint32_t size, uint8_t value) { + // Replicate the fill byte across the value field so value_matches can + // recognise e.g. 0xDEADBEEF only if the byte is 0xDE/0xAD/0xBE/0xEF — for + // capture purposes the byte itself in the low slot is enough. + uint64_t v = static_cast(value); + v |= v << 8; + v |= v << 16; + v |= v << 32; + xe::audit_68::check_guest_va(address, v, + static_cast(std::min(size, 8)), + "Memory::Fill"); std::memset(TranslateVirtual(address), value, size); } void Memory::Copy(uint32_t dest, uint32_t src, uint32_t size) { uint8_t* pdest = TranslateVirtual(dest); const uint8_t* psrc = TranslateVirtual(src); + // AUDIT-068 Session 2: full byte-scan over 4-byte aligned positions of the + // source buffer. Catches XEX-loader-style memcpys where a vptr (the target + // u32 value) is buried somewhere mid-buffer rather than at offset 0. Cost + // O(size/4 * N_values) with N_values capped at 8 inside value_matches — + // negligible vs the underlying memcpy throughput. + // + // Gated on active bit 0x1 (values-mode) AND active != 0. If only addrs are + // configured (Run 2 voice-struct mode), we still emit a single addr-only + // event covering the destination span so addr-watch isn't broken. + uint32_t active = xe::audit_68::g_active.load(std::memory_order_relaxed); + if (active != 0) [[unlikely]] { + if ((active & 0x1) && size >= 4) { + // Scan source for any configured u32 value (big-endian, mirrors how + // guest sees the bytes). 4-byte aligned offsets only. + uint32_t aligned_end = size & ~3u; + for (uint32_t i = 0; i < aligned_end; i += 4) { + uint32_t be_u32 = + (uint32_t(psrc[i + 0]) << 24) | (uint32_t(psrc[i + 1]) << 16) | + (uint32_t(psrc[i + 2]) << 8) | uint32_t(psrc[i + 3]); + xe::audit_68::check_guest_va(dest + i, be_u32, 4, "Memory::Copy"); + } + } + if (active & 0x2) { + // Addr-only mode: emit a single coarse event tagged with the dest base + // and first u32 of source for context. The slow-path range check will + // log iff the dest span intersects a configured addr range. + uint64_t v = 0; + if (size >= 4) { + v = (uint64_t(psrc[0]) << 24) | (uint64_t(psrc[1]) << 16) | + (uint64_t(psrc[2]) << 8) | uint64_t(psrc[3]); + } else if (size > 0) { + for (uint32_t i = 0; i < size; ++i) { + v = (v << 8) | psrc[i]; + } + } + xe::audit_68::check_guest_va( + dest, v, static_cast(std::min(size, 8)), + "Memory::Copy"); + } + } std::memcpy(pdest, psrc, size); } diff --git a/src/xenia/memory.h b/src/xenia/memory.h index bd9519a40..fa712fe08 100644 --- a/src/xenia/memory.h +++ b/src/xenia/memory.h @@ -347,6 +347,13 @@ class Memory { Memory(); ~Memory(); + // AUDIT-068: process-global Memory singleton accessor. Returns the + // currently-constructed Memory instance, or nullptr if none. Set inside + // Memory::Memory()/~Memory(); see memory.cc `active_memory_`. Used by + // xe::audit_68::check_host_write() to translate a host pointer back to a + // guest VA without an explicit Memory* context. + static Memory* active(); + // Initializes the memory system. // This may fail if the host address space could not be reserved or the // mapping to the file system fails. diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index fcdca8bbf..4355d963f 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -332,12 +332,11 @@ else() X86_AVX512VNNI WITH_GZFILEOP ) - if(NOT MSVC) - # GCC/Clang have native __builtin_ctz/__builtin_ctzll (MSVC gets these - # from fallback_builtins.h). Defining these enables optimized compare256 - # and longest_match codepaths. + # Real cl.exe exposes all intrinsics unconditionally; GCC, Clang, AND clang-cl + # all need per-file -m flags to declare them. CMAKE_CXX_COMPILER_ID is + # "Clang" for both Linux Clang and clang-cl. + if(NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_definitions(zlib-ng PRIVATE HAVE_BUILTIN_CTZ HAVE_BUILTIN_CTZLL) - # GCC/Clang need per-file ISA flags; MSVC handles this via runtime dispatch set_source_files_properties( zlib-ng/arch/x86/adler32_avx2.c zlib-ng/arch/x86/chunkset_avx2.c diff --git a/third_party/snappy b/third_party/snappy index 6af9287fb..77c78fad9 160000 --- a/third_party/snappy +++ b/third_party/snappy @@ -1 +1 @@ -Subproject commit 6af9287fbdb913f0794d0148c6aa43b58e63c8e3 +Subproject commit 77c78fad94fc4640149dc7901816159571b04df1 diff --git a/tools/build/compile_shader_dxbc.py b/tools/build/compile_shader_dxbc.py index d191a2125..61442aa48 100644 --- a/tools/build/compile_shader_dxbc.py +++ b/tools/build/compile_shader_dxbc.py @@ -72,15 +72,29 @@ def main(): is_dxc = "dxc" in os.path.basename(fxc).lower() # Start with base command — use wine on non-Windows platforms. + # fxc.exe sees '/foo' as a switch, so unix-rooted paths must be + # translated to Windows form (Z:\...) before being passed. if sys.platform != "win32": + def _wineify(p): + try: + out = subprocess.check_output(["winepath", "-w", p], + stderr=subprocess.DEVNULL) + return out.decode("utf-8", "replace").strip() + except (OSError, subprocess.CalledProcessError): + return p + input_path = _wineify(input_path) + output_path = _wineify(output_path) + src_dir = _wineify(src_dir) compiler_args = ["wine", fxc] else: compiler_args = [fxc] if is_dxc: - # DXC only supports SM 6.0+. + # DXC supports both SM 5.x (-> DXBC) and SM 6.x (-> DXIL). xenia's D3D12 + # backend reads DXBC (output dir is bytecode/d3d12_5_1/), so target + # SM 5_1 to keep DXBC parity with FXC output. compiler_args.extend([ - "-T", f"{stage}_6_0", + "-T", f"{stage}_5_1", "-HV", "2017", "-D", "SHADING_LANGUAGE_HLSL_XE=1", "-I", src_dir,