Source changes (dormant parity infra, retained from iterate 2.AI/2.AO): - xenia-kernel/exports.rs: nt_create_event manual_reset polarity + related event wiring - xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps (.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as regenerable local artifacts — see memory + HANDOFF for the running findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1168 lines
52 KiB
Diff
1168 lines
52 KiB
Diff
diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs
|
|
index a4dfa7d..96d23bf 100644
|
|
--- a/crates/xenia-kernel/src/exports.rs
|
|
+++ b/crates/xenia-kernel/src/exports.rs
|
|
@@ -46,8 +46,14 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", ke_query_base_priority_thread);
|
|
state.register_export(Xboxkrnl, 0x82, "KeQueryIdealProcessor", ke_query_ideal_processor);
|
|
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
|
|
- state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
|
|
- state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero);
|
|
+ // Canary declares `void KeQuerySystemTime_entry(lpqword_t time_ptr, ...)`
|
|
+ // (xboxkrnl_threading.cc:459); the time is delivered via the OUT
|
|
+ // pointer, not via gpr[3]. Phase A's `kernel.return.return_value`
|
|
+ // must be 0 (canary literal) — not r3 (which for ours is the input
|
|
+ // arg `time_ptr` left untouched). See `register_void_export` doc in
|
|
+ // state.rs.
|
|
+ state.register_void_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
|
|
+ state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", ke_raise_irql_to_dpc_level);
|
|
state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", ke_release_semaphore);
|
|
state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", ke_release_spinlock_from_raised_irql);
|
|
state.register_export(Xboxkrnl, 0x8F, "KeResetEvent", ke_reset_event);
|
|
@@ -61,7 +67,7 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0xAF, "KeWaitForMultipleObjects", ke_wait_for_multiple_objects);
|
|
state.register_export(Xboxkrnl, 0xB0, "KeWaitForSingleObject", ke_wait_for_single_object);
|
|
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", kf_acquire_spin_lock);
|
|
- state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", stub_success);
|
|
+ state.register_void_export(Xboxkrnl, 0xB3, "KfLowerIrql", kf_lower_irql);
|
|
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", kf_release_spin_lock);
|
|
state.register_export(Xboxkrnl, 0x0152, "KeTlsAlloc", ke_tls_alloc);
|
|
state.register_export(Xboxkrnl, 0x0153, "KeTlsFree", stub_success);
|
|
@@ -132,7 +138,7 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x0126, "RtlFillMemoryUlong", rtl_fill_memory_ulong);
|
|
state.register_export(Xboxkrnl, 0x0127, "RtlFreeAnsiString", stub_success);
|
|
state.register_export(Xboxkrnl, 0x012B, "RtlImageXexHeaderField", rtl_image_xex_header_field);
|
|
- state.register_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string);
|
|
+ state.register_void_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string);
|
|
state.register_export(Xboxkrnl, 0x012D, "RtlInitUnicodeString", rtl_init_unicode_string);
|
|
state.register_export(Xboxkrnl, 0x012E, "RtlInitializeCriticalSection", rtl_initialize_critical_section);
|
|
state.register_export(Xboxkrnl, 0x012F, "RtlInitializeCriticalSectionAndSpinCount", rtl_initialize_critical_section);
|
|
@@ -185,8 +191,8 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success);
|
|
|
|
// Crypto
|
|
- state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success);
|
|
- state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", stub_success);
|
|
+ state.register_void_export(Xboxkrnl, 0x0192, "XeCryptSha", xe_crypt_sha);
|
|
+ state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", xe_keys_console_private_key_sign);
|
|
state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
|
|
|
|
// Xex module
|
|
@@ -453,18 +459,33 @@ fn nt_set_information_thread(
|
|
}
|
|
}
|
|
|
|
-/// `KeSetAffinityThread(thread_handle, new_mask) -> old_mask` — Axis 4.
|
|
-/// Drives `KernelState::set_affinity` which delegates to the scheduler
|
|
-/// and then fixes up every outstanding `ThreadRef` held in waiter lists.
|
|
+/// `KeSetAffinityThread(thread_ptr, affinity, prev_affinity_ptr)` — Axis 4.
|
|
+/// Mirrors xenia-canary `KeSetAffinityThread_entry`
|
|
+/// (xboxkrnl_threading.cc:323-346): returns `X_STATUS_SUCCESS` (0) in r3
|
|
+/// and writes the previous affinity to `*prev_affinity_ptr` (r5) when
|
|
+/// non-NULL. Validates `affinity != 0` (else `X_STATUS_INVALID_PARAMETER`)
|
|
+/// and that the thread handle resolves (else `X_STATUS_INVALID_HANDLE`).
|
|
+///
|
|
+/// Stage 2 Batch 3 fix (2026-05-14): pre-fix, ours returned `old_mask` in
|
|
+/// r3 with no OUT-pointer write — guest code expecting `STATUS_SUCCESS`
|
|
+/// in r3 was reading a small bitmask as an NTSTATUS.
|
|
fn ke_set_affinity_thread(
|
|
ctx: &mut PpcContext,
|
|
mem: &GuestMemory,
|
|
state: &mut KernelState,
|
|
) {
|
|
- let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
|
let new_mask = (ctx.gpr[4] as u32) as u8;
|
|
+ let prev_ptr = ctx.gpr[5] as u32;
|
|
+ if new_mask == 0 {
|
|
+ ctx.gpr[3] = 0xC000_000D; // X_STATUS_INVALID_PARAMETER
|
|
+ return;
|
|
+ }
|
|
+ let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
|
let old = state.set_affinity(handle, new_mask, mem);
|
|
- ctx.gpr[3] = old as u64;
|
|
+ if prev_ptr != 0 {
|
|
+ mem.write_u32(prev_ptr, old as u32);
|
|
+ }
|
|
+ ctx.gpr[3] = 0; // X_STATUS_SUCCESS
|
|
}
|
|
|
|
fn ke_bug_check(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
|
@@ -495,6 +516,49 @@ fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut Ke
|
|
}
|
|
}
|
|
|
|
+/// Offset of `current_irql` (u8) within PCR. Mirrors xenia-canary's
|
|
+/// `X_KPCR.current_irql` at offset 0x18 (xthread.h:189). PCR base is in
|
|
+/// `ctx.gpr[13]` per scheduler setup.
|
|
+const PCR_CURRENT_IRQL_OFFSET: u32 = 0x18;
|
|
+
|
|
+/// Mirrors xenia-canary `KeRaiseIrqlToDpcLevel_entry`
|
|
+/// (xboxkrnl_threading.cc:1253-1264): reads PCR's `current_irql`,
|
|
+/// returns the old value in r3, writes `DISPATCH_LEVEL` (2) back.
|
|
+fn ke_raise_irql_to_dpc_level(
|
|
+ ctx: &mut PpcContext,
|
|
+ mem: &GuestMemory,
|
|
+ _state: &mut KernelState,
|
|
+) {
|
|
+ let pcr = ctx.gpr[13] as u32;
|
|
+ let old_irql = mem.read_u8(pcr.wrapping_add(PCR_CURRENT_IRQL_OFFSET));
|
|
+ if old_irql > 2 {
|
|
+ tracing::warn!(
|
|
+ old_irql = old_irql,
|
|
+ "KeRaiseIrqlToDpcLevel: old_irql > 2 (DISPATCH_LEVEL)"
|
|
+ );
|
|
+ }
|
|
+ mem.write_u8(pcr.wrapping_add(PCR_CURRENT_IRQL_OFFSET), 2);
|
|
+ ctx.gpr[3] = old_irql as u64;
|
|
+}
|
|
+
|
|
+/// Mirrors xenia-canary `KfLowerIrql_entry`
|
|
+/// (xboxkrnl_threading.cc:1280-1282 calling `xeKfLowerIrql`): writes
|
|
+/// `new_irql` (r3) to PCR's `current_irql`. Void return (registered via
|
|
+/// `register_void_export`).
|
|
+fn kf_lower_irql(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
|
+ let new_irql = (ctx.gpr[3] as u32) as u8;
|
|
+ let pcr = ctx.gpr[13] as u32;
|
|
+ let current = mem.read_u8(pcr.wrapping_add(PCR_CURRENT_IRQL_OFFSET));
|
|
+ if new_irql > current {
|
|
+ tracing::warn!(
|
|
+ new_irql = new_irql,
|
|
+ current = current,
|
|
+ "KfLowerIrql: new_irql > current_irql"
|
|
+ );
|
|
+ }
|
|
+ mem.write_u8(pcr.wrapping_add(PCR_CURRENT_IRQL_OFFSET), new_irql);
|
|
+}
|
|
+
|
|
fn ke_initialize_semaphore(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
|
// r3 = PKSEMAPHORE, r4 = initial count, r5 = limit.
|
|
// Mirrors xenia-canary KeInitializeSemaphore_entry
|
|
@@ -592,8 +656,102 @@ fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut Kernel
|
|
ctx.gpr[3] = 1; // TRUE
|
|
}
|
|
|
|
-fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
|
- ctx.gpr[3] = 0; // STATUS_SUCCESS (writes nothing)
|
|
+/// Mirrors xenia-canary `ExGetXConfigSetting_entry` + `xeExGetXConfigSetting`
|
|
+/// (xboxkrnl_xconfig.cc:303-319 calling :65-302). Returns a small value
|
|
+/// describing one of the Xbox 360's `XCONFIG_*` settings.
|
|
+///
|
|
+/// Stage 2 Batch 6 (2026-05-14): pre-fix returned STATUS_SUCCESS with no
|
|
+/// buffer write — game saw uninitialized buffer data. We implement the
|
|
+/// most commonly queried (category, setting) pairs as constants matching
|
|
+/// canary's defaults. Unknown pairs return `STATUS_INVALID_PARAMETER_2`.
|
|
+fn ex_get_xconfig_setting(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
|
+ let category = (ctx.gpr[3] as u32) & 0xFFFF;
|
|
+ let setting = (ctx.gpr[4] as u32) & 0xFFFF;
|
|
+ let buffer_ptr = ctx.gpr[5] as u32;
|
|
+ let buffer_size = (ctx.gpr[6] as u32) & 0xFFFF;
|
|
+ let required_size_ptr = ctx.gpr[7] as u32;
|
|
+
|
|
+ // Per-setting value encoded as big-endian bytes (canary uses
|
|
+ // `xe::store_and_swap<T>`; we hand-roll the BE bytes since values
|
|
+ // are constant).
|
|
+ #[derive(Clone, Copy)]
|
|
+ enum SettingValue {
|
|
+ U8(u8),
|
|
+ U16Be(u16),
|
|
+ U32Be(u32),
|
|
+ }
|
|
+ impl SettingValue {
|
|
+ fn size(&self) -> u16 {
|
|
+ match self {
|
|
+ SettingValue::U8(_) => 1,
|
|
+ SettingValue::U16Be(_) => 2,
|
|
+ SettingValue::U32Be(_) => 4,
|
|
+ }
|
|
+ }
|
|
+ fn write(&self, mem: &GuestMemory, addr: u32) {
|
|
+ match self {
|
|
+ SettingValue::U8(v) => mem.write_u8(addr, *v),
|
|
+ SettingValue::U16Be(v) => mem.write_u16(addr, *v),
|
|
+ SettingValue::U32Be(v) => mem.write_u32(addr, *v),
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let value: Option<SettingValue> = match (category, setting) {
|
|
+ // XCONFIG_SECURED_CATEGORY = 0x02
|
|
+ (0x02, 0x02) => Some(SettingValue::U32Be(1)), // SECURED_AV_REGION = NTSCM
|
|
+ // XCONFIG_USER_CATEGORY = 0x03
|
|
+ (0x03, 0x01) // TIME_ZONE_BIAS
|
|
+ | (0x03, 0x02) // TIME_ZONE_STD_NAME
|
|
+ | (0x03, 0x03) // TIME_ZONE_DLT_NAME
|
|
+ | (0x03, 0x04) // TIME_ZONE_STD_DATE
|
|
+ | (0x03, 0x05) // TIME_ZONE_DLT_DATE
|
|
+ | (0x03, 0x06) // TIME_ZONE_STD_BIAS
|
|
+ | (0x03, 0x07) // TIME_ZONE_DLT_BIAS
|
|
+ => Some(SettingValue::U32Be(0)),
|
|
+ (0x03, 0x09) => Some(SettingValue::U32Be(1)), // USER_LANGUAGE = en
|
|
+ (0x03, 0x0A) => Some(SettingValue::U32Be(0)), // USER_VIDEO_FLAGS = RatioNormal
|
|
+ (0x03, 0x0B) => Some(SettingValue::U32Be(0x00010001)), // USER_AUDIO_FLAGS
|
|
+ (0x03, 0x0C) => Some(SettingValue::U32Be(0x40)), // USER_RETAIL_FLAGS
|
|
+ (0x03, 0x0E) => Some(SettingValue::U8(103)), // USER_COUNTRY = US
|
|
+ (0x03, 0x0F) => Some(SettingValue::U8(0x03)), // USER_PC_FLAGS = XBL allowed
|
|
+ // XCONFIG_CONSOLE_CATEGORY = 0x07
|
|
+ (0x07, 0x02) => Some(SettingValue::U16Be(0)), // SCREEN_SAVER = Off
|
|
+ (0x07, 0x03) => Some(SettingValue::U16Be(0)), // AUTO_SHUT_OFF = Off
|
|
+ _ => None,
|
|
+ };
|
|
+
|
|
+ let v = match value {
|
|
+ Some(v) => v,
|
|
+ None => {
|
|
+ // Unknown category or setting. Match canary's per-category
|
|
+ // return code: invalid category vs invalid setting both
|
|
+ // surface as STATUS_INVALID_PARAMETER_x in canary; we use
|
|
+ // STATUS_INVALID_PARAMETER_2 as a single sentinel since the
|
|
+ // distinction is rarely consulted by guest code.
|
|
+ ctx.gpr[3] = 0xC000_00F0; // X_STATUS_INVALID_PARAMETER_2
|
|
+ return;
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let setting_size = v.size();
|
|
+
|
|
+ if buffer_ptr != 0 {
|
|
+ if buffer_size < setting_size as u32 {
|
|
+ ctx.gpr[3] = 0xC000_0023; // X_STATUS_BUFFER_TOO_SMALL
|
|
+ return;
|
|
+ }
|
|
+ v.write(mem, buffer_ptr);
|
|
+ } else if buffer_size != 0 {
|
|
+ ctx.gpr[3] = 0xC000_00F1; // X_STATUS_INVALID_PARAMETER_3
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if required_size_ptr != 0 {
|
|
+ mem.write_u16(required_size_ptr, setting_size);
|
|
+ }
|
|
+
|
|
+ ctx.gpr[3] = 0; // STATUS_SUCCESS
|
|
}
|
|
|
|
// ===== Memory =====
|
|
@@ -730,6 +888,25 @@ const STATUS_SEMAPHORE_LIMIT_EXCEEDED: u64 = 0xC000_0047;
|
|
const STATUS_UNSUCCESSFUL: u64 = 0xC000_0001;
|
|
const STATUS_INVALID_INFO_CLASS: u64 = 0xC000_0003;
|
|
const STATUS_INFO_LENGTH_MISMATCH: u64 = 0xC000_0004;
|
|
+/// Phase C+5 — canary's `NtWriteFile_entry`
|
|
+/// (xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:351-353) returns
|
|
+/// this NT-style status code when the underlying `XFile::is_synchronous_`
|
|
+/// is false (i.e. the file was opened without `FILE_SYNCHRONOUS_IO_ALERT`
|
|
+/// or `FILE_SYNCHRONOUS_IO_NONALERT`). The write itself still completes
|
|
+/// synchronously and the IO_STATUS_BLOCK still records STATUS_SUCCESS;
|
|
+/// only the function return value flips. Real NT uses STATUS_PENDING here
|
|
+/// as a "the caller may now wait on the event" convention.
|
|
+const STATUS_PENDING: u64 = 0x0000_0103;
|
|
+
|
|
+/// `CreateOptions` bits we care about for is-synchronous tracking
|
|
+/// (canary's `CreateOptions::FILE_SYNCHRONOUS_IO_ALERT` /
|
|
+/// `CreateOptions::FILE_SYNCHRONOUS_IO_NONALERT` in xboxkrnl_io.cc:32-33).
|
|
+/// `NtOpenFile` forwards the same options dword through its `open_options`
|
|
+/// argument, so this bitmask applies to both paths.
|
|
+const FILE_SYNCHRONOUS_IO_ALERT: u32 = 0x0000_0010;
|
|
+const FILE_SYNCHRONOUS_IO_NONALERT: u32 = 0x0000_0020;
|
|
+const FILE_SYNCHRONOUS_IO_MASK: u32 =
|
|
+ FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT;
|
|
/// `X_ERROR_NOT_FOUND` from xenia-canary `xenia/xbox.h`. Returned by
|
|
/// `XexGetModuleHandle` for unknown module names.
|
|
const X_ERROR_NOT_FOUND: u64 = 0x0000_048B;
|
|
@@ -737,6 +914,17 @@ const X_ERROR_NOT_FOUND: u64 = 0x0000_048B;
|
|
/// A sentinel byte-offset value meaning "read at current file position".
|
|
const FILE_USE_FILE_POINTER_POSITION: u64 = 0xFFFF_FFFF_FFFF_FFFE;
|
|
|
|
+/// Phase C+5 — register `handle` in `state.async_file_handles` iff the
|
|
+/// caller did NOT request synchronous IO (mirrors canary's
|
|
+/// `XFile::is_synchronous_` derivation in xboxkrnl_io.cc:94-97). Subsequent
|
|
+/// `nt_write_file` returns flip from `STATUS_SUCCESS` to `STATUS_PENDING`
|
|
+/// for async-opened files only.
|
|
+fn maybe_mark_async_file(state: &mut KernelState, handle: u32, create_options: u32) {
|
|
+ if (create_options & FILE_SYNCHRONOUS_IO_MASK) == 0 {
|
|
+ state.async_file_handles.insert(handle);
|
|
+ }
|
|
+}
|
|
+
|
|
/// Write an `IO_STATUS_BLOCK { status, information }` if the pointer is non-null.
|
|
fn write_io_status_block(mem: &GuestMemory, ptr: u32, status: u32, information: u32) {
|
|
if ptr == 0 {
|
|
@@ -836,6 +1024,7 @@ fn open_cache_file(
|
|
dir_enum_pos: None,
|
|
host_path: None,
|
|
});
|
|
+ maybe_mark_async_file(state, handle, create_options);
|
|
if handle_out != 0 {
|
|
mem.write_u32(handle_out, handle);
|
|
}
|
|
@@ -931,6 +1120,7 @@ fn open_cache_file(
|
|
dir_enum_pos: None,
|
|
host_path: Some(host_path.to_path_buf()),
|
|
});
|
|
+ maybe_mark_async_file(state, handle, create_options);
|
|
if handle_out != 0 {
|
|
mem.write_u32(handle_out, handle);
|
|
}
|
|
@@ -1004,6 +1194,7 @@ fn open_vfs_file(
|
|
dir_enum_pos: None,
|
|
host_path: None,
|
|
});
|
|
+ maybe_mark_async_file(state, handle, create_options);
|
|
if handle_out != 0 {
|
|
mem.write_u32(handle_out, handle);
|
|
}
|
|
@@ -1047,6 +1238,7 @@ fn open_vfs_file(
|
|
dir_enum_pos: None,
|
|
host_path: None,
|
|
});
|
|
+ maybe_mark_async_file(state, handle, create_options);
|
|
if handle_out != 0 {
|
|
mem.write_u32(handle_out, handle);
|
|
}
|
|
@@ -1085,6 +1277,7 @@ fn open_vfs_file(
|
|
dir_enum_pos: None,
|
|
host_path: None,
|
|
});
|
|
+ maybe_mark_async_file(state, handle, create_options);
|
|
if handle_out != 0 {
|
|
mem.write_u32(handle_out, handle);
|
|
}
|
|
@@ -1122,16 +1315,26 @@ fn nt_create_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelSta
|
|
}
|
|
|
|
fn nt_open_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|
- // r3 = handle_out, r4 = desired_access, r5 = obj_attrs,
|
|
- // r6 = io_status_block, r7 = share_access, r8 = open_options.
|
|
- // `NtOpenFile` is FILE_OPEN-only (no create) — file must exist.
|
|
- // Per xboxkrnl_io.cc:99-122, NtOpenFile forwards `open_options`
|
|
+ // Phase C+5 — canary `NtOpenFile_entry`
|
|
+ // (xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:114-122) has
|
|
+ // FIVE args: (handle_out, desired_access, object_attributes,
|
|
+ // io_status_block, open_options). Per Xenia's shim_utils LoadValue
|
|
+ // (util/shim_utils.h:158-167), the 5th dword arg arrives in r7. Ours
|
|
+ // previously read r8 — the bit 0x01 (FILE_DIRECTORY_FILE) check still
|
|
+ // happened to pass because the game also left bit 0x01 set in r8 for
|
|
+ // dir opens (AUDIT-054 enabling condition), but the
|
|
+ // FILE_SYNCHRONOUS_IO_NONALERT bit (0x20) was wrongly set in r8 for
|
|
+ // device opens, making every file appear synchronous and causing the
|
|
+ // Phase C+5 NtWriteFile divergence at idx=102068
|
|
+ // (canary=STATUS_PENDING / ours=STATUS_SUCCESS).
|
|
+ //
|
|
+ // Per xboxkrnl_io.cc:118-122, NtOpenFile forwards `open_options`
|
|
// straight into NtCreateFile's `create_options` slot, so the
|
|
- // FILE_DIRECTORY_FILE bit applies the same way.
|
|
+ // FILE_DIRECTORY_FILE bit + sync bits apply the same way.
|
|
let handle_out = ctx.gpr[3] as u32;
|
|
let obj_attrs_ptr = ctx.gpr[5] as u32;
|
|
let io_status_block = ctx.gpr[6] as u32;
|
|
- let open_options = ctx.gpr[8] as u32;
|
|
+ let open_options = ctx.gpr[7] as u32;
|
|
ctx.gpr[3] = open_vfs_file(
|
|
mem,
|
|
state,
|
|
@@ -1320,6 +1523,7 @@ fn nt_write_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelStat
|
|
*position
|
|
};
|
|
|
|
+ let mut wrote_ok = false;
|
|
if let Some(hp) = host_path.clone() {
|
|
use std::io::{Seek, SeekFrom, Write};
|
|
let mut buf = vec![0u8; length as usize];
|
|
@@ -1341,6 +1545,7 @@ fn nt_write_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelStat
|
|
*size = live_size;
|
|
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, length);
|
|
ctx.gpr[3] = STATUS_SUCCESS;
|
|
+ wrote_ok = true;
|
|
tracing::info!(
|
|
"NtWriteFile cache: {} bytes to {:?} @ {} (handle={:#x})",
|
|
length, path, start_pos, handle
|
|
@@ -1356,6 +1561,19 @@ fn nt_write_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelStat
|
|
// Legacy: discard but report full-length-written so caller proceeds.
|
|
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, length);
|
|
ctx.gpr[3] = STATUS_SUCCESS;
|
|
+ wrote_ok = true;
|
|
+ }
|
|
+ // Phase C+5 — canary `NtWriteFile_entry`
|
|
+ // (xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:351-353) flips
|
|
+ // the function return value to `STATUS_PENDING` after the synchronous
|
|
+ // write completes when the underlying `XFile::is_synchronous_` is
|
|
+ // false. The IO_STATUS_BLOCK already stores STATUS_SUCCESS above; only
|
|
+ // the r3 return changes. Mirroring this here closes the
|
|
+ // `tid_event_idx=102068` divergence (canary=0x103 / ours=0) on the
|
|
+ // main thread without touching `NtReadFile` / `NtReadFileScatter`
|
|
+ // (scoped to one divergence per Phase C session, per project plan).
|
|
+ if wrote_ok && state.async_file_handles.contains(&handle) {
|
|
+ ctx.gpr[3] = STATUS_PENDING;
|
|
}
|
|
signal_io_completion_event(state, event_handle);
|
|
}
|
|
@@ -1936,6 +2154,10 @@ fn nt_close(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
|
if remaining == 0 {
|
|
state.objects.remove(&handle);
|
|
state.handle_refcount.remove(&handle);
|
|
+ // Phase C+5 — prune the async-file side-table when the underlying
|
|
+ // handle is finally released. Mirrors the canary `XFile` dtor
|
|
+ // releasing `is_synchronous_`. No-op for non-file handles.
|
|
+ state.async_file_handles.remove(&handle);
|
|
// If the object was an armed Timer, strip its pending-fire entry
|
|
// so a later scheduler round doesn't try to signal a dead handle.
|
|
// `disarm_timer` is a no-op for non-timer handles.
|
|
@@ -2382,10 +2604,79 @@ fn rtl_fill_memory_ulong(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut K
|
|
}
|
|
}
|
|
|
|
-fn rtl_image_xex_header_field(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
|
- // r3 = xex_header_ptr, r4 = field_id
|
|
- // Return 0 for all fields
|
|
- ctx.gpr[3] = 0;
|
|
+fn rtl_image_xex_header_field(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|
+ // r3 = xex_header_guest_ptr (may be NULL — game's CRT often passes 0
|
|
+ // because ours's `*XexExecutableModuleHandle = image_base` doesn't
|
|
+ // resolve to a real LDR_DATA_TABLE_ENTRY, so its `*(hmodule + 0x58)`
|
|
+ // deref yields PE OptionalHeader bytes instead of a header pointer;
|
|
+ // those bytes fail the game's validation and the call goes through
|
|
+ // with ptr=NULL). When NULL, fall back to KernelState's recorded
|
|
+ // `xex_header_guest_ptr` (the guest-VA of the raw XEX header copy
|
|
+ // set up in `xenia-app::cmd_exec`'s Phase 3, mirroring canary's
|
|
+ // `user_module.cc:223-227` `guest_xex_header_`).
|
|
+ // r4 = field_key (xex2_header_keys).
|
|
+ //
|
|
+ // Mirror of canary's `xboxkrnl_rtl.cc:501-514` →
|
|
+ // `UserModule::GetOptHeader(memory, header, key, &field_value)`
|
|
+ // (`user_module.cc:335-369`). Iterates `header->headers[]` (flat
|
|
+ // array of (key:u32, value:u32) pairs, both BE), and for the first
|
|
+ // entry where `opt_header.key == key` returns one of:
|
|
+ // * key & 0xFF == 0x00 → `opt_header.value` (inline value).
|
|
+ // * key & 0xFF == 0x01 → guest VA of `opt_header.value` itself.
|
|
+ // * else → `header_base + opt_header.offset`
|
|
+ // i.e. guest VA inside the header of the referenced data block.
|
|
+ // Returns 0 if the resolved header pointer is NULL or the key is
|
|
+ // not found.
|
|
+ let mut xex_header_ptr = ctx.gpr[3] as u32;
|
|
+ let field_key = ctx.gpr[4] as u32;
|
|
+ if xex_header_ptr == 0 {
|
|
+ xex_header_ptr = state.xex_header_guest_ptr;
|
|
+ }
|
|
+ if xex_header_ptr == 0 {
|
|
+ ctx.gpr[3] = 0;
|
|
+ return;
|
|
+ }
|
|
+ // xex2_header layout (raw, BE; see xenia-canary `xex2_info.h`):
|
|
+ // +0x00 magic ("XEX2"), +0x04 module_flags, +0x08 header_size,
|
|
+ // +0x0C reserved, +0x10 security_offset, +0x14 header_count,
|
|
+ // +0x18.. array of (key:u32, value:u32) pairs.
|
|
+ let header_count = mem.read_u32(xex_header_ptr.wrapping_add(0x14));
|
|
+ let entries_base = xex_header_ptr.wrapping_add(0x18);
|
|
+ let mut field_value: u32 = 0;
|
|
+ let mut found = false;
|
|
+ for i in 0..header_count {
|
|
+ let entry_addr = entries_base.wrapping_add(i.wrapping_mul(8));
|
|
+ let entry_key = mem.read_u32(entry_addr);
|
|
+ if entry_key != field_key {
|
|
+ continue;
|
|
+ }
|
|
+ found = true;
|
|
+ let entry_value_addr = entry_addr.wrapping_add(4);
|
|
+ match entry_key & 0xFF {
|
|
+ 0x00 => {
|
|
+ // Inline value.
|
|
+ field_value = mem.read_u32(entry_value_addr);
|
|
+ }
|
|
+ 0x01 => {
|
|
+ // Pointer to the inline value slot itself.
|
|
+ field_value = entry_value_addr;
|
|
+ }
|
|
+ _ => {
|
|
+ // Offset within the header. `opt_header.value` here is the
|
|
+ // file offset of the optional data block, which canary
|
|
+ // copied verbatim into guest memory at `xex_header_ptr`,
|
|
+ // so `xex_header_ptr + offset` is the in-guest VA.
|
|
+ let offset = mem.read_u32(entry_value_addr);
|
|
+ field_value = xex_header_ptr.wrapping_add(offset);
|
|
+ }
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ if !found {
|
|
+ ctx.gpr[3] = 0;
|
|
+ return;
|
|
+ }
|
|
+ ctx.gpr[3] = field_value as u64;
|
|
}
|
|
|
|
fn rtl_multi_byte_to_unicode_n(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
|
@@ -3266,6 +3557,78 @@ fn xma_create_context(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut Kern
|
|
ctx.gpr[3] = handle as u64;
|
|
}
|
|
|
|
+// ===== Crypto =====
|
|
+
|
|
+/// Mirrors xenia-canary `XeCryptSha_entry` (xboxkrnl_crypt.cc:469-489):
|
|
+/// 3-input SHA-1 accumulator. Each of the three (ptr, size) pairs is
|
|
+/// processed only when both ptr and size are non-zero. The resulting
|
|
+/// 20-byte digest is copied to `output`, truncated to `output_size`.
|
|
+/// Void return (registered via `register_void_export`).
|
|
+fn xe_crypt_sha(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
|
+ use sha1::{Digest, Sha1};
|
|
+ let input_1 = ctx.gpr[3] as u32;
|
|
+ let input_1_size = ctx.gpr[4] as u32;
|
|
+ let input_2 = ctx.gpr[5] as u32;
|
|
+ let input_2_size = ctx.gpr[6] as u32;
|
|
+ let input_3 = ctx.gpr[7] as u32;
|
|
+ let input_3_size = ctx.gpr[8] as u32;
|
|
+ let output = ctx.gpr[9] as u32;
|
|
+ let output_size = ctx.gpr[10] as u32;
|
|
+ let mut hasher = Sha1::new();
|
|
+ for (ptr, size) in [
|
|
+ (input_1, input_1_size),
|
|
+ (input_2, input_2_size),
|
|
+ (input_3, input_3_size),
|
|
+ ] {
|
|
+ if ptr != 0 && size != 0 {
|
|
+ let mut buf = vec![0u8; size as usize];
|
|
+ mem.read_bytes(ptr, &mut buf);
|
|
+ hasher.update(&buf);
|
|
+ }
|
|
+ }
|
|
+ let digest = hasher.finalize();
|
|
+ let n = std::cmp::min(20, output_size as usize);
|
|
+ if output != 0 && n != 0 {
|
|
+ mem.write_bytes(output, &digest[..n]);
|
|
+ }
|
|
+}
|
|
+
|
|
+/// Mirrors xenia-canary `XeKeysConsolePrivateKeySign_entry`
|
|
+/// (xboxkrnl_crypt.cc:1111-1138): writes a hardcoded fake
|
|
+/// `XE_CONSOLE_CERTIFICATE` (0x1A8 bytes) to `output` and returns 1
|
|
+/// (success). Returns 0 if either pointer is null. The 5-byte
|
|
+/// `XE_CONSOLE_ID` bit-field at offset 0x02 is laid out per MSVC
|
|
+/// `#pragma pack(1)` semantics; we write the precomputed bytes
|
|
+/// directly to avoid bit-fiddling ambiguity.
|
|
+fn xe_keys_console_private_key_sign(
|
|
+ ctx: &mut PpcContext,
|
|
+ mem: &GuestMemory,
|
|
+ _state: &mut KernelState,
|
|
+) {
|
|
+ let hash = ctx.gpr[3] as u32;
|
|
+ let output = ctx.gpr[4] as u32;
|
|
+ if hash == 0 || output == 0 {
|
|
+ ctx.gpr[3] = 0;
|
|
+ return;
|
|
+ }
|
|
+ // Zero the 0x1A8-byte struct first (canary calls `output.Zero()`).
|
|
+ let zeros = [0u8; 0x1A8];
|
|
+ mem.write_bytes(output, &zeros);
|
|
+ // XE_CONSOLE_ID at offset 0x02 (5 bytes, MSVC pack(1) bit-fields).
|
|
+ // RefurbBits = 0b0011, ManufactureMonth = 0b1001 → byte 0 = 0x93
|
|
+ // ManufactureYear = 1, MacIndex3 = 0x40, MacIndex4 = 0x66,
|
|
+ // MacIndex5 = 0x7E, Crc = 0 → bytes 1..5 = 0x01,0x64,0xE6,0x07
|
|
+ // (LSB-first packing of the 32-bit storage unit at offset 1.)
|
|
+ let console_id = [0x93u8, 0x01, 0x64, 0xE6, 0x07];
|
|
+ mem.write_bytes(output + 0x02, &console_id);
|
|
+ // console_type (u32 BE) at 0x18 → Retail = 2
|
|
+ mem.write_u32(output + 0x18, 2);
|
|
+ // manufacture_date[8] at 0x1C
|
|
+ let mfg_date = [2u8, 0, 0, 5, 1, 1, 2, 2];
|
|
+ mem.write_bytes(output + 0x1C, &mfg_date);
|
|
+ ctx.gpr[3] = 1;
|
|
+}
|
|
+
|
|
// ===== Xex =====
|
|
|
|
/// Mirrors xenia-canary `XexCheckExecutablePrivilege_entry`
|
|
@@ -4423,12 +4786,21 @@ mod tests {
|
|
// Confirm PCR was written by the spawn (sanity).
|
|
assert_eq!(mem.read_u32(pcr_base + 0x2C), 1);
|
|
|
|
- // Now call KeSetAffinityThread(handle=0x2000, new_mask=0x20).
|
|
+ // Now call KeSetAffinityThread(handle=0x2000, new_mask=0x20,
|
|
+ // prev_mask_ptr=scratch). Post Stage 2 Batch 3: r3=STATUS_SUCCESS,
|
|
+ // previous mask delivered via OUT-pointer.
|
|
+ let prev_ptr = SCRATCH_BASE + 0xA0;
|
|
+ mem.write_u32(prev_ptr, 0xFFFF_FFFF); // sentinel
|
|
ctx.gpr[3] = 0x2000;
|
|
ctx.gpr[4] = 0x20; // slot 5 only
|
|
+ ctx.gpr[5] = prev_ptr as u64;
|
|
ke_set_affinity_thread(&mut ctx, &mut mem, &mut state);
|
|
- // Return value = previous mask = 0x02.
|
|
- assert_eq!(ctx.gpr[3], 0x02);
|
|
+ assert_eq!(ctx.gpr[3], 0, "must return STATUS_SUCCESS in r3");
|
|
+ assert_eq!(
|
|
+ mem.read_u32(prev_ptr),
|
|
+ 0x02,
|
|
+ "previous affinity mask must be written to OUT-pointer"
|
|
+ );
|
|
// PCR rewritten to 5.
|
|
assert_eq!(mem.read_u32(pcr_base + 0x2C), 5);
|
|
// Thread now on slot 5.
|
|
@@ -4436,6 +4808,58 @@ mod tests {
|
|
assert_eq!(r.hw_id, 5);
|
|
}
|
|
|
|
+ /// Stage 2 Batch 3: zero affinity must return STATUS_INVALID_PARAMETER
|
|
+ /// and not touch the OUT-pointer.
|
|
+ #[test]
|
|
+ fn ke_set_affinity_thread_zero_affinity_returns_invalid_parameter() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let prev_ptr = SCRATCH_BASE + 0xA0;
|
|
+ mem.write_u32(prev_ptr, 0xDEAD_BEEF);
|
|
+ ctx.gpr[3] = 0x1000; // main handle
|
|
+ ctx.gpr[4] = 0; // zero affinity
|
|
+ ctx.gpr[5] = prev_ptr as u64;
|
|
+ ke_set_affinity_thread(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0xC000_000D, "STATUS_INVALID_PARAMETER");
|
|
+ assert_eq!(mem.read_u32(prev_ptr), 0xDEAD_BEEF, "OUT-ptr untouched");
|
|
+ }
|
|
+
|
|
+ /// Stage 2 Batch 3: NULL OUT-pointer is valid (mirrors canary's
|
|
+ /// `if (previous_affinity_ptr)` guard); still returns SUCCESS and
|
|
+ /// migrates the thread.
|
|
+ #[test]
|
|
+ fn ke_set_affinity_thread_null_out_ptr_still_succeeds() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ use xenia_cpu::scheduler::SpawnParams;
|
|
+ let pcr_base = SCRATCH_BASE + 0x500;
|
|
+ let params = SpawnParams {
|
|
+ entry: 0x8200_0000,
|
|
+ start_context: 0,
|
|
+ stack_base: 0x7200_0000,
|
|
+ stack_size: 0x10000,
|
|
+ pcr_base,
|
|
+ tls_base: 0,
|
|
+ thread_handle: 0x2100,
|
|
+ guest_tid: 43,
|
|
+ create_suspended: false,
|
|
+ is_initial: false,
|
|
+ tls_slot_count: 0,
|
|
+ affinity_mask: 0b0000_0010,
|
|
+ priority: 0,
|
|
+ ideal_processor: None,
|
|
+ };
|
|
+ state
|
|
+ .scheduler
|
|
+ .spawn(params, &mut crate::state::GuestMemoryPcr(&mut mem))
|
|
+ .unwrap();
|
|
+ ctx.gpr[3] = 0x2100;
|
|
+ ctx.gpr[4] = 0x10; // slot 4
|
|
+ ctx.gpr[5] = 0; // NULL OUT-ptr
|
|
+ ke_set_affinity_thread(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS even with NULL OUT-ptr");
|
|
+ let r = state.scheduler.find_by_handle(0x2100).expect("alive");
|
|
+ assert_eq!(r.hw_id, 4);
|
|
+ }
|
|
+
|
|
/// Axis 5: `KeSetIdealProcessor` stores a hint on the thread
|
|
/// without migrating it; query round-trips.
|
|
#[test]
|
|
@@ -4660,6 +5084,94 @@ mod tests {
|
|
assert!(event_signaled(&state, evt), "write must signal too");
|
|
}
|
|
|
|
+ /// Phase C+5 — async-opened files (no `FILE_SYNCHRONOUS_IO_*` bit in
|
|
+ /// `create_options`) return `STATUS_PENDING` (0x103) from
|
|
+ /// `NtWriteFile`. The synchronous write still completes and
|
|
+ /// IO_STATUS_BLOCK still records STATUS_SUCCESS — only the function
|
|
+ /// return value flips. Mirrors canary
|
|
+ /// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:351-353`.
|
|
+ #[test]
|
|
+ fn nt_write_file_async_handle_returns_status_pending() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ // Pre-register an "async" file handle the same way `open_vfs_file`
|
|
+ // does for a file whose `create_options` omits sync bits.
|
|
+ let handle = state.alloc_handle_for(KernelObject::File {
|
|
+ path: "async.tmp".to_string(),
|
|
+ size: 0,
|
|
+ position: 0,
|
|
+ data: std::sync::Arc::new(Vec::new()),
|
|
+ dir_enum_pos: None,
|
|
+ host_path: None,
|
|
+ });
|
|
+ state.async_file_handles.insert(handle);
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ ctx.gpr[4] = 0; // no event
|
|
+ ctx.gpr[7] = SCRATCH_BASE as u64; // iosb at scratch base
|
|
+ ctx.gpr[9] = 8; // length
|
|
+ nt_write_file(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], STATUS_PENDING,
|
|
+ "async-opened file: r3 must return STATUS_PENDING (0x103)"
|
|
+ );
|
|
+ assert_eq!(
|
|
+ mem.read_u32(SCRATCH_BASE),
|
|
+ STATUS_SUCCESS as u32,
|
|
+ "IO_STATUS_BLOCK.status still records STATUS_SUCCESS"
|
|
+ );
|
|
+ assert_eq!(
|
|
+ mem.read_u32(SCRATCH_BASE + 4),
|
|
+ 8,
|
|
+ "IO_STATUS_BLOCK.information records bytes written"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ /// Sync-opened files (one of `FILE_SYNCHRONOUS_IO_*` bits set in
|
|
+ /// `create_options`) retain the legacy `STATUS_SUCCESS` return.
|
|
+ #[test]
|
|
+ fn nt_write_file_sync_handle_returns_status_success() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let handle = state.alloc_handle_for(KernelObject::File {
|
|
+ path: "sync.tmp".to_string(),
|
|
+ size: 0,
|
|
+ position: 0,
|
|
+ data: std::sync::Arc::new(Vec::new()),
|
|
+ dir_enum_pos: None,
|
|
+ host_path: None,
|
|
+ });
|
|
+ // Not inserted into `async_file_handles` — sync handle by default.
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ ctx.gpr[4] = 0;
|
|
+ ctx.gpr[7] = SCRATCH_BASE as u64;
|
|
+ ctx.gpr[9] = 8;
|
|
+ nt_write_file(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], STATUS_SUCCESS,
|
|
+ "sync-opened file: r3 must return STATUS_SUCCESS"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ /// `nt_close` must prune the async-file side-table when the final
|
|
+ /// refcount drops to zero so a recycled handle isn't mis-classified.
|
|
+ #[test]
|
|
+ fn nt_close_prunes_async_file_set() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let handle = state.alloc_handle_for(KernelObject::File {
|
|
+ path: "x.tmp".to_string(),
|
|
+ size: 0,
|
|
+ position: 0,
|
|
+ data: std::sync::Arc::new(Vec::new()),
|
|
+ dir_enum_pos: None,
|
|
+ host_path: None,
|
|
+ });
|
|
+ state.async_file_handles.insert(handle);
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ nt_close(&mut ctx, &mem, &mut state);
|
|
+ assert!(
|
|
+ !state.async_file_handles.contains(&handle),
|
|
+ "nt_close must remove from async_file_handles"
|
|
+ );
|
|
+ }
|
|
+
|
|
/// Verify `FileStandardInformation` reports `Directory=1` for empty-path
|
|
/// (device-root) synthesized file handles. Sylpheed calls
|
|
/// `NtCreateFile("game:\\")` then `NtQueryInformationFile` on the returned
|
|
@@ -6215,6 +6727,14 @@ mod tests {
|
|
let obj_attrs = write_obj_attrs(&mem, SCRATCH_BASE + 0x100, "cache:\\rt.tmp");
|
|
let handle_out = SCRATCH_BASE + 0x300;
|
|
let iosb = SCRATCH_BASE + 0x310;
|
|
+ // Phase C+5 — set sp so nt_create_file reads create_options from a
|
|
+ // committed scratch slot, and set the FILE_SYNCHRONOUS_IO_NONALERT
|
|
+ // bit so `NtWriteFile` returns `STATUS_SUCCESS` (legacy assertion).
|
|
+ // Files opened WITHOUT this bit return `STATUS_PENDING` after
|
|
+ // canary's xboxkrnl_io.cc:351-353 — covered by
|
|
+ // `nt_write_file_async_handle_returns_status_pending`.
|
|
+ ctx.gpr[1] = (SCRATCH_BASE + 0x700) as u64;
|
|
+ mem.write_u32(SCRATCH_BASE + 0x700 + 0x54, FILE_SYNCHRONOUS_IO_NONALERT);
|
|
ctx.gpr[3] = handle_out as u64;
|
|
ctx.gpr[5] = obj_attrs as u64;
|
|
ctx.gpr[6] = iosb as u64;
|
|
@@ -6353,4 +6873,214 @@ mod tests {
|
|
assert!(resolved.ends_with("etc/foo"));
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
+
|
|
+ // ===== Stage 2 Batch 2: Crypto handlers =====
|
|
+
|
|
+ #[test]
|
|
+ fn xe_crypt_sha_empty_input_writes_canonical_digest() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let input_ptr = SCRATCH_BASE;
|
|
+ let output_ptr = SCRATCH_BASE + 0x100;
|
|
+ ctx.gpr[3] = input_ptr as u64;
|
|
+ ctx.gpr[4] = 0; // input_1_size = 0 (skips this buffer)
|
|
+ ctx.gpr[5] = 0;
|
|
+ ctx.gpr[6] = 0;
|
|
+ ctx.gpr[7] = 0;
|
|
+ ctx.gpr[8] = 0;
|
|
+ ctx.gpr[9] = output_ptr as u64;
|
|
+ ctx.gpr[10] = 20;
|
|
+ xe_crypt_sha(&mut ctx, &mem, &mut state);
|
|
+ let mut got = [0u8; 20];
|
|
+ mem.read_bytes(output_ptr, &mut got);
|
|
+ // SHA-1 of empty input
|
|
+ let expected: [u8; 20] = [
|
|
+ 0xDA, 0x39, 0xA3, 0xEE, 0x5E, 0x6B, 0x4B, 0x0D, 0x32, 0x55, 0xBF, 0xEF, 0x95, 0x60,
|
|
+ 0x18, 0x90, 0xAF, 0xD8, 0x07, 0x09,
|
|
+ ];
|
|
+ assert_eq!(got, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn xe_crypt_sha_three_inputs_concatenate() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let buf_a = SCRATCH_BASE;
|
|
+ let buf_b = SCRATCH_BASE + 0x10;
|
|
+ let buf_c = SCRATCH_BASE + 0x20;
|
|
+ let output_ptr = SCRATCH_BASE + 0x100;
|
|
+ mem.write_bytes(buf_a, b"abc");
|
|
+ mem.write_bytes(buf_b, b"def");
|
|
+ mem.write_bytes(buf_c, b"ghi");
|
|
+ ctx.gpr[3] = buf_a as u64;
|
|
+ ctx.gpr[4] = 3;
|
|
+ ctx.gpr[5] = buf_b as u64;
|
|
+ ctx.gpr[6] = 3;
|
|
+ ctx.gpr[7] = buf_c as u64;
|
|
+ ctx.gpr[8] = 3;
|
|
+ ctx.gpr[9] = output_ptr as u64;
|
|
+ ctx.gpr[10] = 20;
|
|
+ xe_crypt_sha(&mut ctx, &mem, &mut state);
|
|
+ let mut got = [0u8; 20];
|
|
+ mem.read_bytes(output_ptr, &mut got);
|
|
+ // SHA-1("abcdefghi") = c63b19f1e4c8b5f76b25c49b8b87f57d8e4872a1
|
|
+ let expected: [u8; 20] = [
|
|
+ 0xC6, 0x3B, 0x19, 0xF1, 0xE4, 0xC8, 0xB5, 0xF7, 0x6B, 0x25, 0xC4, 0x9B, 0x8B, 0x87,
|
|
+ 0xF5, 0x7D, 0x8E, 0x48, 0x72, 0xA1,
|
|
+ ];
|
|
+ assert_eq!(got, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn xe_crypt_sha_truncates_output() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let output_ptr = SCRATCH_BASE + 0x100;
|
|
+ // Pre-fill 0xFF so we can verify only 4 bytes were written.
|
|
+ mem.write_bytes(output_ptr, &[0xFFu8; 20]);
|
|
+ ctx.gpr[3] = 0;
|
|
+ ctx.gpr[4] = 0;
|
|
+ ctx.gpr[5] = 0;
|
|
+ ctx.gpr[6] = 0;
|
|
+ ctx.gpr[7] = 0;
|
|
+ ctx.gpr[8] = 0;
|
|
+ ctx.gpr[9] = output_ptr as u64;
|
|
+ ctx.gpr[10] = 4; // truncate to 4 bytes
|
|
+ xe_crypt_sha(&mut ctx, &mem, &mut state);
|
|
+ // First 4 bytes match SHA-1 of empty; next 16 stay 0xFF.
|
|
+ let mut got = [0u8; 20];
|
|
+ mem.read_bytes(output_ptr, &mut got);
|
|
+ assert_eq!(&got[..4], &[0xDA, 0x39, 0xA3, 0xEE]);
|
|
+ assert_eq!(&got[4..], &[0xFFu8; 16]);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn xe_keys_console_private_key_sign_writes_certificate_and_returns_one() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let hash_ptr = SCRATCH_BASE;
|
|
+ let output_ptr = SCRATCH_BASE + 0x100;
|
|
+ ctx.gpr[3] = hash_ptr as u64;
|
|
+ ctx.gpr[4] = output_ptr as u64;
|
|
+ xe_keys_console_private_key_sign(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 1, "must return success");
|
|
+ // console_type at 0x18 (u32 BE) = Retail (2)
|
|
+ assert_eq!(mem.read_u32(output_ptr + 0x18), 2);
|
|
+ // manufacture_date at 0x1C
|
|
+ let mut mfg = [0u8; 8];
|
|
+ mem.read_bytes(output_ptr + 0x1C, &mut mfg);
|
|
+ assert_eq!(mfg, [2, 0, 0, 5, 1, 1, 2, 2]);
|
|
+ // XE_CONSOLE_ID byte 0 at offset 0x02
|
|
+ assert_eq!(mem.read_u8(output_ptr + 0x02), 0x93);
|
|
+ // cert_size and console_part_number must remain zero (Zero() output)
|
|
+ assert_eq!(mem.read_u16(output_ptr), 0);
|
|
+ assert_eq!(mem.read_u8(output_ptr + 0x07), 0);
|
|
+ }
|
|
+
|
|
+ // ===== Stage 2 Batch 6: ExGetXConfigSetting =====
|
|
+
|
|
+ #[test]
|
|
+ fn ex_get_xconfig_setting_user_language_returns_one() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let buf = SCRATCH_BASE + 0x200;
|
|
+ let req = SCRATCH_BASE + 0x208;
|
|
+ mem.write_u32(buf, 0xDEAD_BEEF);
|
|
+ mem.write_u16(req, 0xFFFF);
|
|
+ ctx.gpr[3] = 0x03; // USER_CATEGORY
|
|
+ ctx.gpr[4] = 0x09; // USER_LANGUAGE
|
|
+ ctx.gpr[5] = buf as u64;
|
|
+ ctx.gpr[6] = 4;
|
|
+ ctx.gpr[7] = req as u64;
|
|
+ ex_get_xconfig_setting(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS");
|
|
+ assert_eq!(mem.read_u32(buf), 1, "USER_LANGUAGE = en");
|
|
+ assert_eq!(mem.read_u16(req), 4, "required_size = 4 bytes");
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn ex_get_xconfig_setting_unknown_returns_invalid_parameter() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let buf = SCRATCH_BASE + 0x200;
|
|
+ ctx.gpr[3] = 0xDEAD;
|
|
+ ctx.gpr[4] = 0xBEEF;
|
|
+ ctx.gpr[5] = buf as u64;
|
|
+ ctx.gpr[6] = 4;
|
|
+ ctx.gpr[7] = 0;
|
|
+ ex_get_xconfig_setting(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0xC000_00F0, "STATUS_INVALID_PARAMETER_2");
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn ex_get_xconfig_setting_buffer_too_small_returns_error() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let buf = SCRATCH_BASE + 0x200;
|
|
+ mem.write_u32(buf, 0xDEAD_BEEF);
|
|
+ ctx.gpr[3] = 0x03; // USER_CATEGORY
|
|
+ ctx.gpr[4] = 0x09; // USER_LANGUAGE (4 bytes)
|
|
+ ctx.gpr[5] = buf as u64;
|
|
+ ctx.gpr[6] = 2; // too small
|
|
+ ctx.gpr[7] = 0;
|
|
+ ex_get_xconfig_setting(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0xC000_0023, "STATUS_BUFFER_TOO_SMALL");
|
|
+ // Buffer untouched
|
|
+ assert_eq!(mem.read_u32(buf), 0xDEAD_BEEF);
|
|
+ }
|
|
+
|
|
+ // ===== Stage 2 Batch 5: IRQL pair =====
|
|
+
|
|
+ /// Stage 2 Batch 5: `KeRaiseIrqlToDpcLevel` reads PCR's current_irql,
|
|
+ /// returns it in r3, and writes DISPATCH_LEVEL=2 back.
|
|
+ #[test]
|
|
+ fn ke_raise_irql_to_dpc_level_returns_old_writes_dispatch_level() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let pcr = SCRATCH_BASE + 0x500;
|
|
+ // Initial IRQL = PASSIVE_LEVEL (0).
|
|
+ mem.write_u8(pcr + PCR_CURRENT_IRQL_OFFSET, 0);
|
|
+ ctx.gpr[13] = pcr as u64;
|
|
+ ke_raise_irql_to_dpc_level(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0, "old IRQL = PASSIVE_LEVEL");
|
|
+ assert_eq!(
|
|
+ mem.read_u8(pcr + PCR_CURRENT_IRQL_OFFSET),
|
|
+ 2,
|
|
+ "PCR.current_irql = DISPATCH_LEVEL"
|
|
+ );
|
|
+ // Second Raise returns 2 (already at DPC).
|
|
+ ke_raise_irql_to_dpc_level(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 2);
|
|
+ assert_eq!(mem.read_u8(pcr + PCR_CURRENT_IRQL_OFFSET), 2);
|
|
+ }
|
|
+
|
|
+ /// Stage 2 Batch 5: Raise → Lower round-trip leaves PCR at the value
|
|
+ /// passed to Lower. Demonstrates the IRQL nesting invariant.
|
|
+ #[test]
|
|
+ fn ke_irql_raise_lower_round_trip() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let pcr = SCRATCH_BASE + 0x500;
|
|
+ mem.write_u8(pcr + PCR_CURRENT_IRQL_OFFSET, 0);
|
|
+ ctx.gpr[13] = pcr as u64;
|
|
+ ke_raise_irql_to_dpc_level(&mut ctx, &mem, &mut state);
|
|
+ let prev = ctx.gpr[3] as u8;
|
|
+ assert_eq!(prev, 0);
|
|
+ assert_eq!(mem.read_u8(pcr + PCR_CURRENT_IRQL_OFFSET), 2);
|
|
+ // Restore.
|
|
+ ctx.gpr[3] = prev as u64;
|
|
+ kf_lower_irql(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(
|
|
+ mem.read_u8(pcr + PCR_CURRENT_IRQL_OFFSET),
|
|
+ 0,
|
|
+ "PCR.current_irql restored to PASSIVE_LEVEL"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn xe_keys_console_private_key_sign_rejects_null_inputs() {
|
|
+ let (mut ctx, mem, mut state) = fresh();
|
|
+ let output_ptr = SCRATCH_BASE + 0x100;
|
|
+ // null hash
|
|
+ ctx.gpr[3] = 0;
|
|
+ ctx.gpr[4] = output_ptr as u64;
|
|
+ xe_keys_console_private_key_sign(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0, "must return failure on null hash");
|
|
+ // null output
|
|
+ ctx.gpr[3] = 0x1234_5678;
|
|
+ ctx.gpr[4] = 0;
|
|
+ xe_keys_console_private_key_sign(&mut ctx, &mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 0, "must return failure on null output");
|
|
+ }
|
|
}
|
|
diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs
|
|
index b256fe7..32c7218 100644
|
|
--- a/crates/xenia-kernel/src/state.rs
|
|
+++ b/crates/xenia-kernel/src/state.rs
|
|
@@ -50,6 +50,17 @@ pub const HMODULE_XAM: u32 = 0xFFFE_0002;
|
|
/// Central kernel state tracking all guest OS state.
|
|
pub struct KernelState {
|
|
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
|
|
+ /// Phase A: kernel exports whose canary signature is `void` (no
|
|
+ /// dword_result_t / pointer_result_t). For symmetry with canary's
|
|
+ /// `if constexpr (std::is_void<R>::value)` trampoline branch
|
|
+ /// (see `xenia-canary/src/xenia/kernel/util/shim_utils.h`), the
|
|
+ /// Phase A `kernel.return` event for these exports emits
|
|
+ /// `return_value=0` instead of `gpr[3]` (which for void fns is
|
|
+ /// just the input arg pointer left untouched). Without this,
|
|
+ /// e.g. `KeQuerySystemTime` — declared `void` in canary, taking a
|
|
+ /// `lpqword_t time_ptr` — would report ours's r3=time_ptr but
|
|
+ /// canary's literal 0, producing a spurious diff. Cvar-OFF inert.
|
|
+ void_exports: std::collections::HashSet<(ModuleId, u32)>,
|
|
/// M2.4: bump allocator for kernel handles. `AtomicU32` so concurrent
|
|
/// HLE calls under M3 can `fetch_add` without a lock. `Relaxed` is
|
|
/// fine — the allocated value is a fresh ID with no prior payload to
|
|
@@ -70,6 +81,16 @@ pub struct KernelState {
|
|
pub cs_waiters: HashMap<u32, Vec<ThreadRef>>,
|
|
/// Kernel object table: handle → object
|
|
pub objects: HashMap<u32, KernelObject>,
|
|
+ /// Phase C+5 — set of file handles opened WITHOUT
|
|
+ /// `FILE_SYNCHRONOUS_IO_ALERT` (0x10) or `FILE_SYNCHRONOUS_IO_NONALERT`
|
|
+ /// (0x20). Canary's `NtWriteFile_entry`
|
|
+ /// (xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:351-353)
|
|
+ /// completes such writes synchronously but returns `STATUS_PENDING`
|
|
+ /// (0x103) instead of `STATUS_SUCCESS`. Mirrors `xfile.is_synchronous_`
|
|
+ /// in canary (xfile.h:177, xfile.cc:22). Populated by `open_vfs_file`
|
|
+ /// and `open_cache_file`; pruned by `nt_close` when the handle's
|
|
+ /// refcount drops to zero.
|
|
+ pub async_file_handles: std::collections::HashSet<u32>,
|
|
/// Bump allocator for guest heap (NtAllocateVirtualMemory etc.).
|
|
/// M2.4: `AtomicU32` for lock-free concurrent allocation.
|
|
pub heap_cursor: std::sync::atomic::AtomicU32,
|
|
@@ -91,6 +112,17 @@ pub struct KernelState {
|
|
pub last_input_bytes: u128,
|
|
/// Image base of the loaded XEX (for XexExecutableModuleHandle etc.)
|
|
pub image_base: u32,
|
|
+ /// Guest VA of the raw XEX header bytes copied into guest memory at
|
|
+ /// startup (mirrors canary's `UserModule::guest_xex_header_`,
|
|
+ /// allocated in `user_module.cc:224`). Used by `RtlImageXexHeaderField`
|
|
+ /// to compute return values that are offsets into the in-guest header
|
|
+ /// copy (canary's `xboxkrnl_rtl.cc:501-514` calls `UserModule::Get
|
|
+ /// OptHeader(memory, header, key, &field_value)` which iterates
|
|
+ /// `header->headers[]` and returns `HostToGuestVirtual(header) +
|
|
+ /// opt_header.offset` for "else"-class keys, key low byte != 0/1). Zero
|
|
+ /// when the executable hasn't been installed yet. Set once by
|
|
+ /// `xenia-app` after `mem.write_bulk(base, &image_data)`.
|
|
+ pub xex_header_guest_ptr: 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
|
|
@@ -264,6 +296,23 @@ pub struct KernelState {
|
|
pub dump_addrs: Vec<u32>,
|
|
/// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`.
|
|
pub dump_section: Option<(u32, u32, std::path::PathBuf)>,
|
|
+ /// Phase B initial-state snapshot — directory under which a
|
|
+ /// `ours/{cpu_state,memory,kernel,vfs,config}.json` + `manifest.json`
|
|
+ /// snapshot is written at the moment immediately before the first
|
|
+ /// guest PPC instruction of the XEX entry_point. `None` (default) =
|
|
+ /// disabled, zero overhead. See
|
|
+ /// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
|
|
+ pub phase_b_snapshot_dir: Option<std::path::PathBuf>,
|
|
+ /// Phase B: after writing the snapshot, exit the process immediately
|
|
+ /// so re-runs are byte-deterministic. Default false.
|
|
+ pub phase_b_snapshot_and_exit: bool,
|
|
+ /// Phase B: include raw bytes in `memory.json`'s `section_contents`.
|
|
+ /// Default false — per-region SHA-256 is enough for the routine diff.
|
|
+ pub phase_b_dump_section_content: bool,
|
|
+ /// Phase B: the XEX entry_point address — captured by the app at
|
|
+ /// `install_initial_thread` time and consulted by the snapshot hook
|
|
+ /// to validate the firing thread is the entry thread.
|
|
+ pub entry_pc: u32,
|
|
}
|
|
|
|
impl KernelState {
|
|
@@ -288,11 +337,13 @@ impl KernelState {
|
|
scheduler.set_reservation_table(Some(reservations.clone()));
|
|
let mut state = Self {
|
|
exports: HashMap::new(),
|
|
+ void_exports: std::collections::HashSet::new(),
|
|
next_handle: AtomicU32::new(0x1000),
|
|
scheduler,
|
|
next_tls_index: AtomicU32::new(0),
|
|
cs_waiters: HashMap::new(),
|
|
objects: HashMap::new(),
|
|
+ async_file_handles: std::collections::HashSet::new(),
|
|
heap_cursor: AtomicU32::new(0x4000_0000), // Start of user heap region
|
|
stack_cursor: AtomicU32::new(0x7100_0000), // Above main stack
|
|
gpu_command_buffer: 0,
|
|
@@ -300,6 +351,7 @@ impl KernelState {
|
|
input_packet_number: 0,
|
|
last_input_bytes: 0,
|
|
image_base: 0,
|
|
+ xex_header_guest_ptr: 0,
|
|
xex_system_flags: 0,
|
|
xex_priv_logged: std::collections::HashSet::new(),
|
|
has_notified_startup: false,
|
|
@@ -331,6 +383,10 @@ impl KernelState {
|
|
lr_trace_writer: None,
|
|
dump_addrs: Vec::new(),
|
|
dump_section: None,
|
|
+ phase_b_snapshot_dir: None,
|
|
+ phase_b_snapshot_and_exit: false,
|
|
+ phase_b_dump_section_content: false,
|
|
+ entry_pc: 0,
|
|
};
|
|
crate::exports::register_exports(&mut state);
|
|
crate::xam::register_exports(&mut state);
|
|
@@ -377,6 +433,22 @@ impl KernelState {
|
|
self.exports.insert((module, ordinal), (name, func));
|
|
}
|
|
|
|
+ /// Register a kernel export whose canary signature is `void`.
|
|
+ /// See `KernelState::void_exports` doc. Identical semantics to
|
|
+ /// `register_export` except the Phase A `kernel.return` payload's
|
|
+ /// `return_value` field is emitted as 0 instead of `gpr[3]`,
|
|
+ /// matching canary's `EmitReturn(name, 0)` branch.
|
|
+ pub fn register_void_export(
|
|
+ &mut self,
|
|
+ module: ModuleId,
|
|
+ ordinal: u32,
|
|
+ name: &'static str,
|
|
+ func: KernelExportFn,
|
|
+ ) {
|
|
+ self.exports.insert((module, ordinal), (name, func));
|
|
+ self.void_exports.insert((module, ordinal));
|
|
+ }
|
|
+
|
|
/// AUDIT-038 — install a host directory as the backing store for the
|
|
/// `cache:` mount. The directory is unconditionally cleared (and then
|
|
/// re-created) on entry so two consecutive runs see byte-identical
|
|
@@ -514,7 +586,49 @@ impl KernelState {
|
|
metrics::counter!("kernel.calls", "name" => name).increment(1);
|
|
tracing::trace!(target: "probe_calls", "hw={} call={} r3={:#x} r4={:#x} r5={:#x} lr={:#x}",
|
|
r.hw_id, name, ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.lr);
|
|
+ // Phase A event log — see crates/xenia-kernel/src/event_log.rs.
|
|
+ // Hot path: `is_enabled` is a relaxed atomic-bool load.
|
|
+ let phase_a_on = crate::event_log::is_enabled();
|
|
+ let (phase_a_tid, phase_a_cycle) = if phase_a_on {
|
|
+ let tid = self.scheduler.thread(r).tid;
|
|
+ let cycle = ctx.cycle_count;
|
|
+ (tid, cycle)
|
|
+ } else {
|
|
+ (0u32, 0u64)
|
|
+ };
|
|
+ if phase_a_on {
|
|
+ let module_name = match module {
|
|
+ ModuleId::Xboxkrnl => "xboxkrnl.exe",
|
|
+ ModuleId::Xam => "xam.xex",
|
|
+ ModuleId::Xbdm => "xbdm.xex",
|
|
+ };
|
|
+ crate::event_log::emit_import_call(
|
|
+ phase_a_tid,
|
|
+ phase_a_cycle,
|
|
+ module_name,
|
|
+ ordinal as u16,
|
|
+ name,
|
|
+ );
|
|
+ crate::event_log::emit_kernel_call(phase_a_tid, phase_a_cycle, name);
|
|
+ }
|
|
+ let is_void = self.void_exports.contains(&(module, ordinal));
|
|
func(&mut ctx, mem, self);
|
|
+ if phase_a_on {
|
|
+ // Mirror canary's `if constexpr (std::is_void<R>::value)`
|
|
+ // trampoline branch: void exports emit literal 0; non-void
|
|
+ // emit post-call gpr[3]. Without this, void exports that
|
|
+ // take a pointer arg (e.g. `KeQuerySystemTime`) would
|
|
+ // report ours=r3=arg_ptr vs canary=0 — a Phase A diff
|
|
+ // that is purely an emitter-framing asymmetry, not an
|
|
+ // engine semantic divergence.
|
|
+ let return_value = if is_void { 0 } else { ctx.gpr[3] };
|
|
+ crate::event_log::emit_kernel_return(
|
|
+ phase_a_tid,
|
|
+ ctx.cycle_count,
|
|
+ name,
|
|
+ return_value,
|
|
+ );
|
|
+ }
|
|
true
|
|
} else {
|
|
metrics::counter!("kernel.unimplemented").increment(1);
|