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`; 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 = 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::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>, /// Kernel object table: handle → object pub objects: HashMap, + /// 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, /// 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, /// `--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, + /// 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::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);