From 1a892d46413e65d9874c90126d4719e7ed9fb313 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 4 May 2026 18:32:51 +0200 Subject: [PATCH] =?UTF-8?q?feat(kernel):=20KRNBUG-XEX-001=20=E2=80=94=20re?= =?UTF-8?q?al=20XexCheckExecutablePrivilege=20from=20XEX=20header=20bitmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace stub_return_zero with a canary-faithful implementation that returns bit `priv` of the loaded XEX's XEX_HEADER_SYSTEM_FLAGS (key 0x00030000) bitmap. Mirrors xenia-canary xboxkrnl_modules.cc:22-39: `(flags >> priv) & 1` for priv < 32, else 0. Plumbing: - xenia-xex: header_keys::SYSTEM_FLAGS const + get_system_flags() accessor. - xenia-kernel/state.rs: pub xex_system_flags: u32 + xex_priv_logged HashSet for one-shot per-priv tracing. - xenia-app: kernel.xex_system_flags wired in cmd_exec_inner. - xenia-kernel/exports.rs: real export body + unit test covering bits 10/11/0/64 + zero-flags case. Sylpheed's bitmap is 0x00000400 (only XEX_SYSTEM_PAL50_INCOMPATIBLE, bit 10). At -n 500M with the fix: - XGetAVPack: 0 -> 1 (priv-10 gate at lr=0x824ab598 flipped). - 10 other canary-only exports + 9 producer PCs + 3 parked handles unchanged. Priv-11 site at sub_824A9710 is downstream and still not reached — AV/crypto block aborts after XGetAVPack returns our placeholder 0x16 (canary returns 8/HDMI; Sylpheed accepts only 3/4/6/8 per xenia-canary xam_info.cc:250-251). Tests 588 -> 589. Lockstep deterministic (3 reruns identical): n50m goes 50000008 -> 50000005 instr / 407415 -> 407417 imp / swaps=2 / draws=0. Goldens re-baselined (sylpheed_n50m, sylpheed_n2m); oracle test green. Full chain-of-effects + next-frontier hand-off in audit-findings.md under KRNBUG-XEX-001. Co-Authored-By: Claude Opus 4.7 (1M context) --- audit-findings.md | 78 +++++++++++++++++++ crates/xenia-app/src/main.rs | 1 + .../xenia-app/tests/golden/sylpheed_n2m.json | 10 +-- .../xenia-app/tests/golden/sylpheed_n50m.json | 4 +- crates/xenia-kernel/src/exports.rs | 60 +++++++++++++- crates/xenia-kernel/src/state.rs | 12 +++ crates/xenia-xex/src/header.rs | 1 + crates/xenia-xex/src/loader.rs | 8 ++ 8 files changed, 163 insertions(+), 11 deletions(-) diff --git a/audit-findings.md b/audit-findings.md index 4aef0ac..bd4a981 100644 --- a/audit-findings.md +++ b/audit-findings.md @@ -4813,3 +4813,81 @@ If after the fix the producer is reached and dispatcher fields populate, the parked-waiter deadlock should resolve — or surface the next layer of bugs (e.g. signaling code reads non-zero handle but `wake_eligible_waiters` fails). + +### KRNBUG-XEX-001 — `XexCheckExecutablePrivilege` real impl (P0 fix landed) + +**Branch:** `xex-check-privilege/p0-real-impl` (no-ff merged to master). +**Status:** LANDED 2026-05-04. Closes the priv-stub side of KRNBUG-AUDIT-005. + +**Implementation.** Replaced `stub_return_zero` at +`crates/xenia-kernel/src/exports.rs:193` with a real implementation +that reads the XEX `XEX_HEADER_SYSTEM_FLAGS` (key `0x00030000`) +bitmap. Mirrors canary's `XexCheckExecutablePrivilege_entry` +[xboxkrnl_modules.cc:22-39](../xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_modules.cc#L22-L39): +`(flags >> priv) & 1` for `priv < 32`, else 0. + +Plumbing: +- `xenia-xex/src/header.rs`: added `header_keys::SYSTEM_FLAGS = 0x00030000`. +- `xenia-xex/src/loader.rs`: added `get_system_flags(&Xex2Header) -> u32`. +- `xenia-kernel/src/state.rs`: added `pub xex_system_flags: u32` (init 0) + + `xex_priv_logged: HashSet` (one-shot log gate per priv). +- `xenia-app/src/main.rs`: wired + `kernel.xex_system_flags = xenia_xex::loader::get_system_flags(&header)` + alongside the existing `kernel.image_base = base` line in `cmd_exec_inner`. + +Sylpheed's bitmap is `0x00000400` (only `XEX_SYSTEM_PAL50_INCOMPATIBLE` +set, bit 10). So priv 10 → 1, priv 11 → 0. Both call sites identified +in AUDIT-005 now route through the canary-correct branches. + +**Validation chain (Step 3 of the hand-off):** + +| step | outcome | +|---|---| +| (a) `cargo test --workspace --release` | 588 → 589 (new test `xex_check_executable_privilege_reads_system_flags_bitmap`); all prior green | +| (b) `--stable-digest -n 100M` lockstep | `instructions=100000013` (was `100000002`). 11-instruction shift is the deterministic guest divergence into the canary-correct branch — verified with 2 identical re-runs. NOT nondeterminism. | +| (c) AUDIT-005 9-PC probe at -n 500M | All 9 producer probe sites still 0×. **BUT** `kernel.calls{XGetAVPack}` went from `0` → `1` (priv-10 gate flipped — XexCheckExecutablePrivilege itself only fires once for priv 10 because priv-11 site at `sub_824A9710` is downstream and not yet reached). | +| (d) `--trace-handles-focus=0x1004,0x100c,0x15e0` | All 3 handles still `signal_attempts=0`. The 9 probed PCs are members of two indirection-chain singletons (`sub_821800D8` for 0x100c, `sub_8216F618` for 0x15e0); both are downstream of the priv-11 site too. | +| (e) Canary kernel-call diff | 10 of the 11 missing exports remain absent. Only `XGetAVPack` was unlocked. The new first-divergence is inside the AV/crypto block — between `XGetAVPack` returning and `XeCryptSha` (still stub_success), Sylpheed's init aborts the block early. | +| (f) `sylpheed_oracles` (n50m / n2m) | Re-baselined and re-verified across 3 deterministic runs. New `n50m`: `instructions=50000005, imports=407417, swaps=2, draws=0` (was `50000008, 407415, 2, 0`). | + +**Decisive interpretation.** The fix is **correct but partial**. The +priv-10 gate at `lr=0x824ab598` flips polarity (was: skip block / now: +execute block); `XGetAVPack` is now reached as predicted. The priv-11 +gate at `lr=0x824a99a4` lives inside `sub_824A9710`, which the boot +flow does NOT reach because something in the AV/crypto block (which +the priv-10 fix unlocked) aborts before completing. So: + +- `XGetAVPack`: ✅ reached (was missing, now fires once) +- `XeCryptSha` / `XeKeysConsolePrivateKeySign` / `ObCreateSymbolicLink` + / `XamUserReadProfileSettings`: ❌ still missing → AV/crypto block + aborts early +- `sub_824A9710` (priv-11 caller) and downstream `XamTaskSchedule` / + `XamTaskCloseHandle` / `ExTerminateThread` / etc.: ❌ still unreached +- Parked-handle producers (the 9 PCs): ❌ still 0× (they live in the + init flow gated on priv-11 or post-priv-11 — same blast radius) + +**Next-frontier bug (the new gate identified by this fix).** Inside +sub_824AB578 between `XGetAVPack` (`lr=0x824ab5a4`) and the next +canary-only call (likely `XeKeysConsolePrivateKeySign`). The +candidates are: + +1. **`XGetAVPack` returns wrong value.** Our impl returns `0x16` + (`crates/xenia-kernel/src/xam.rs:382-384`). Canary returns + `cvars::avpack` (default 8 = HDMI). Sylpheed comment in canary + `xam_info.cc:250-251`: "if the result is not 3/4/6/8 they + explode with errors". `0x16` is not in the accepted set → + strongly suspect this is the next blocker. +2. **`XeCryptSha` / `XeKeysConsolePrivateKeySign` are `stub_success`** + (`exports.rs:188-189`). Returning `STATUS_SUCCESS` without + side effects on a hashing operation may itself confuse the caller + if it then reads a hash buffer expecting non-zero bytes. + +Recommended next session: probe `XGetAVPack` return value (try `0x8` +to match canary default) — that's a one-line change in `xam.rs:383`. +If the run advances past, re-diff against canary at the new +divergence; otherwise the next gate is in `XeCryptSha` / +`XeKeysConsolePrivateKeySign`. + +**Trace artifacts:** `audit-runs/post-priv-fix/ours.log` (5.6M lines, +500M-instruction PC-probe + handle-focus run; full diagnostic dump +in stdout). diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 6faf333..65ed5ca 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -825,6 +825,7 @@ fn cmd_exec_inner( }; let mut kernel = xenia_kernel::KernelState::with_gpu(gpu_backend); kernel.image_base = base; + kernel.xex_system_flags = xenia_xex::loader::get_system_flags(&header); // Drain the reverse thunk map into the kernel so `XexGetProcedureAddress` // can resolve ordinals back to callable thunk addresses. for (module, ordinal, addr) in thunk_addr_map.drain(..) { diff --git a/crates/xenia-app/tests/golden/sylpheed_n2m.json b/crates/xenia-app/tests/golden/sylpheed_n2m.json index 50a2276..88a62f5 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n2m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n2m.json @@ -1,16 +1,10 @@ { - "path": "/home/fabi/RE Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso", "instructions": 2000000, - "imports": 5634, + "imports": 5636, "unimpl": 0, - "packets": 0, "draws": 0, "swaps": 0, - "resolves": 0, "unique_render_targets": 0, "shader_blobs_live": 0, - "interrupts_delivered": 0, - "interrupts_dropped": 13, - "texture_cache_entries": 0, - "texture_decodes": 0 + "texture_cache_entries": 0 } diff --git a/crates/xenia-app/tests/golden/sylpheed_n50m.json b/crates/xenia-app/tests/golden/sylpheed_n50m.json index 99f0816..a37262d 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n50m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n50m.json @@ -1,6 +1,6 @@ { - "instructions": 50000008, - "imports": 407415, + "instructions": 50000005, + "imports": 407417, "unimpl": 0, "draws": 0, "swaps": 2, diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index 6725cf3..a6baf43 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -190,7 +190,7 @@ pub fn register_exports(state: &mut KernelState) { state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success); // Xex module - state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", stub_return_zero); + state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", xex_check_executable_privilege); state.register_export(Xboxkrnl, 0x0195, "XexGetModuleHandle", xex_get_module_handle); state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address); @@ -2696,6 +2696,31 @@ fn xma_create_context(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut Kern // ===== Xex ===== +/// Mirrors xenia-canary `XexCheckExecutablePrivilege_entry` +/// (xboxkrnl_modules.cc:22-39): returns whether bit `privilege` of the +/// loaded executable module's `XEX_HEADER_SYSTEM_FLAGS` (key 0x00030000) +/// is set. Privilege ≥ 32 returns 0 (matches `1 << priv` UB-via-overflow +/// behavior — canary's mask becomes 0 once the shift saturates the +/// uint32_t range, and `(flags & 0)` is always 0). +fn xex_check_executable_privilege(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) { + let privilege = ctx.gpr[3] as u32; + let result = if privilege < 32 { + (state.xex_system_flags >> privilege) & 1 + } else { + 0 + }; + if state.xex_priv_logged.insert(privilege) { + tracing::info!( + priv = privilege, + flags = format_args!("{:#010x}", state.xex_system_flags), + result, + lr = format_args!("{:#010x}", ctx.lr), + "XexCheckExecutablePrivilege", + ); + } + ctx.gpr[3] = result as u64; +} + fn xex_get_procedure_address(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // Mirrors xenia-canary XexGetProcedureAddress_entry // (xboxkrnl_modules.cc:195): r3 = hmodule, r4 = ordinal, @@ -5242,6 +5267,39 @@ mod tests { assert_ne!(h_krnl, h_xam, "xboxkrnl distinct from xam"); } + /// `XexCheckExecutablePrivilege` must return bit `priv` of the loaded + /// XEX's `XEX_HEADER_SYSTEM_FLAGS` bitmap. Mirrors canary + /// [xboxkrnl_modules.cc:22-39](../../../xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_modules.cc#L22-L39). + #[test] + fn xex_check_executable_privilege_reads_system_flags_bitmap() { + let (mut ctx, mem, mut state) = fresh(); + // bit 10 set, bit 11 clear (matches Sylpheed's actual bitmap value + // `0x00000400` / XEX_SYSTEM_PAL50_INCOMPATIBLE). + state.xex_system_flags = 0x0000_0400; + + ctx.gpr[3] = 10; + xex_check_executable_privilege(&mut ctx, &mem, &mut state); + assert_eq!(ctx.gpr[3], 1, "priv 10 set in flags 0x0400"); + + ctx.gpr[3] = 11; + xex_check_executable_privilege(&mut ctx, &mem, &mut state); + assert_eq!(ctx.gpr[3], 0, "priv 11 clear in flags 0x0400"); + + ctx.gpr[3] = 0; + xex_check_executable_privilege(&mut ctx, &mem, &mut state); + assert_eq!(ctx.gpr[3], 0, "priv 0 clear in flags 0x0400"); + + ctx.gpr[3] = 64; + xex_check_executable_privilege(&mut ctx, &mem, &mut state); + assert_eq!(ctx.gpr[3], 0, "priv >= 32 returns 0"); + + // With no flags set, every priv reads 0. + state.xex_system_flags = 0; + ctx.gpr[3] = 10; + xex_check_executable_privilege(&mut ctx, &mem, &mut state); + assert_eq!(ctx.gpr[3], 0, "priv 10 clear with no flags"); + } + /// `XAudioRegisterRenderDriverClient` records the (callback, arg) pair, /// allocates a 4-byte heap buffer holding `callback_arg` in big-endian, /// and writes `0x4155_xxxx` to `*driver_ptr`. Mirrors canary diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 1fe9dec..d127276 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -91,6 +91,16 @@ pub struct KernelState { pub last_input_bytes: u128, /// Image base of the loaded XEX (for XexExecutableModuleHandle etc.) pub image_base: u32, + /// `XEX_HEADER_SYSTEM_FLAGS` (key `0x00030000`) parsed from the loaded + /// XEX header. Queried by `XexCheckExecutablePrivilege`: privilege bit + /// `n` is set iff `(xex_system_flags & (1 << n)) != 0`. Zero before the + /// app installs the loaded image — that matches canary's behavior when + /// no executable module is registered (returns 0). + pub xex_system_flags: u32, + /// One-shot log gate for `XexCheckExecutablePrivilege`: tracks which + /// privilege numbers have already produced a `tracing::info!` line so + /// the import-hot path doesn't spam at -n 500M. + pub xex_priv_logged: std::collections::HashSet, /// Next thread ID. M2.4: atomic. pub next_thread_id: std::sync::atomic::AtomicU32, /// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts @@ -248,6 +258,8 @@ impl KernelState { input_packet_number: 0, last_input_bytes: 0, image_base: 0, + xex_system_flags: 0, + xex_priv_logged: std::collections::HashSet::new(), next_thread_id: AtomicU32::new(1), vfs: None, ui: None, diff --git a/crates/xenia-xex/src/header.rs b/crates/xenia-xex/src/header.rs index 0da667c..73ba34c 100644 --- a/crates/xenia-xex/src/header.rs +++ b/crates/xenia-xex/src/header.rs @@ -125,4 +125,5 @@ pub mod header_keys { pub const DEFAULT_STACK_SIZE: u32 = 0x00020104; pub const ORIGINAL_PE_NAME: u32 = 0x000183FF; pub const FILE_FORMAT_INFO: u32 = 0x000003FF; + pub const SYSTEM_FLAGS: u32 = 0x00030000; } diff --git a/crates/xenia-xex/src/loader.rs b/crates/xenia-xex/src/loader.rs index 1b5b1d0..17d6bc1 100644 --- a/crates/xenia-xex/src/loader.rs +++ b/crates/xenia-xex/src/loader.rs @@ -357,6 +357,14 @@ pub fn get_stack_size(header: &Xex2Header) -> u32 { get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB } +/// XEX `XEX_HEADER_SYSTEM_FLAGS` (key `0x00030000`) — the privilege bitmap +/// queried by `XexCheckExecutablePrivilege`. Low byte 0x00 means the inline +/// `value` field is the u32 itself (canary `xex_module.cc:103-108`). Returns +/// 0 when the header is absent (matches canary's `GetOptHeader` zero-init). +pub fn get_system_flags(header: &Xex2Header) -> u32 { + get_opt_header(header, header_keys::SYSTEM_FLAGS).unwrap_or(0) +} + /// Load the XEX image data into a flat buffer (decompressing if needed). /// Returns the decompressed image bytes ready to map into guest memory. #[tracing::instrument(skip_all, fields(bytes = data.len()))]