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>
1432 lines
66 KiB
Diff
1432 lines
66 KiB
Diff
diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs
|
|
index a4dfa7d..6374e13 100644
|
|
--- a/crates/xenia-kernel/src/exports.rs
|
|
+++ b/crates/xenia-kernel/src/exports.rs
|
|
@@ -16,7 +16,12 @@ pub fn register_exports(state: &mut KernelState) {
|
|
|
|
// Debug
|
|
state.register_export(Xboxkrnl, 0x01, "DbgBreakPoint", dbg_break_point);
|
|
- state.register_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print);
|
|
+ // Phase C+6½: `DbgPrint` (ord 0x03) is table-entry-only in canary
|
|
+ // (`xboxkrnl_table.inc:17`, no `DECLARE_XBOXKRNL_EXPORT(DbgPrint)`).
|
|
+ // Canary routes through the syscall thunk, which emits NO Phase A
|
|
+ // events. Mirror that — body still logs the string (harmless side
|
|
+ // effect) but the Phase A emitter stays silent.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print);
|
|
|
|
// ExCreateThread and friends
|
|
state.register_export(Xboxkrnl, 0x0D, "ExCreateThread", ex_create_thread);
|
|
@@ -28,7 +33,17 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x28, "HalReturnToFirmware", hal_return_to_firmware);
|
|
|
|
// I/O
|
|
- state.register_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success);
|
|
+ // Phase C+6: `IoDismountVolumeByFileHandle` has a table entry in
|
|
+ // canary's `xboxkrnl_table.inc:74` but NO `DECLARE_XBOXKRNL_EXPORT`
|
|
+ // shim, so canary routes calls through the syscall thunk
|
|
+ // (`xex_module.cc:1310-1335`) which emits NO Phase A events.
|
|
+ // Mirror that by registering as unimplemented — ours still runs
|
|
+ // `stub_success` for guest-visible semantics, but the Phase A
|
|
+ // emitter stays silent. Before this fix, ours's tid=1 main chain
|
|
+ // injected 3 spurious events (`import.call`/`kernel.call`/
|
|
+ // `kernel.return`) at idx=102132 ahead of `NtClose`, becoming the
|
|
+ // first divergence vs canary which jumps straight to `NtClose`.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success);
|
|
|
|
// Ke* Threading/Sync
|
|
state.register_export(Xboxkrnl, 0x4D, "KeAcquireSpinLockAtRaisedIrql", stub_return_zero);
|
|
@@ -44,16 +59,36 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x7D, "KeLeaveCriticalRegion", stub_success);
|
|
state.register_export(Xboxkrnl, 0x7F, "KePulseEvent", ke_pulse_event);
|
|
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", ke_query_base_priority_thread);
|
|
- state.register_export(Xboxkrnl, 0x82, "KeQueryIdealProcessor", ke_query_ideal_processor);
|
|
+ // Phase C+6½ hallucination fix: ord 0x82 = `KeQueryInterruptTime`
|
|
+ // per canary's `xboxkrnl_table.inc:130`. Canary DECLAREs this export
|
|
+ // (`xboxkrnl_misc.cc:127`) — both engines emit Phase A events.
|
|
+ // Previously mis-labeled `KeQueryIdealProcessor` in ours; the body
|
|
+ // returned a wrong value (processor index instead of interrupt-time
|
|
+ // counter). Fixed body returns a synthetic monotonic u64.
|
|
+ state.register_export(Xboxkrnl, 0x82, "KeQueryInterruptTime", ke_query_interrupt_time);
|
|
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);
|
|
state.register_export(Xboxkrnl, 0x92, "KeResumeThread", ke_resume_thread);
|
|
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", ke_set_affinity_thread);
|
|
- state.register_export(Xboxkrnl, 0x98, "KeSetIdealProcessor", ke_set_ideal_processor);
|
|
+ // Phase C+6½ hallucination fix: ord 0x98 = `KeSetBackgroundProcessors`
|
|
+ // per canary's `xboxkrnl_table.inc:166`. Table-entry-only (no
|
|
+ // `DECLARE_XBOXKRNL_EXPORT` shim), so canary routes via the syscall
|
|
+ // thunk and emits NO Phase A events. Previously mis-labeled
|
|
+ // `KeSetIdealProcessor` in ours; the body wrote
|
|
+ // `GuestThread::ideal_processor` — wrong state mutation under the
|
|
+ // wrong name. Replaced with `stub_success` and registered as
|
|
+ // unimplemented to mirror canary's silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x98, "KeSetBackgroundProcessors", stub_success);
|
|
state.register_export(Xboxkrnl, 0x99, "KeSetBasePriorityThread", ke_set_base_priority_thread);
|
|
state.register_export(Xboxkrnl, 0x9B, "KeSetCurrentStackPointers", stub_success);
|
|
state.register_export(Xboxkrnl, 0x9D, "KeSetEvent", ke_set_event);
|
|
@@ -61,7 +96,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);
|
|
@@ -126,13 +161,16 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x0110, "ObReferenceObjectByHandle", ob_reference_object_by_handle);
|
|
|
|
// RTL
|
|
- state.register_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context);
|
|
+ // Phase C+6½: `RtlCaptureContext` (ord 0x119) is table-entry-only
|
|
+ // in canary — no `DECLARE_XBOXKRNL_EXPORT(RtlCaptureContext)`.
|
|
+ // Mirror canary's silence so the Phase A emitter doesn't drift.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context);
|
|
state.register_export(Xboxkrnl, 0x011B, "RtlCompareMemoryUlong", rtl_compare_memory_ulong);
|
|
state.register_export(Xboxkrnl, 0x0125, "RtlEnterCriticalSection", rtl_enter_critical_section);
|
|
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);
|
|
@@ -140,18 +178,27 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x0133, "RtlMultiByteToUnicodeN", rtl_multi_byte_to_unicode_n);
|
|
state.register_export(Xboxkrnl, 0x0135, "RtlNtStatusToDosError", rtl_nt_status_to_dos_error);
|
|
state.register_export(Xboxkrnl, 0x0136, "RtlRaiseException", rtl_raise_exception);
|
|
- state.register_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf);
|
|
+ // Phase C+6½: `sprintf` (ord 0x13B) is table-entry-only in canary
|
|
+ // — no `DECLARE_XBOXKRNL_EXPORT(sprintf)`. Mirror canary's silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf);
|
|
state.register_export(Xboxkrnl, 0x013F, "RtlTimeFieldsToTime", stub_success);
|
|
state.register_export(Xboxkrnl, 0x0140, "RtlTimeToTimeFields", stub_success);
|
|
state.register_export(Xboxkrnl, 0x0141, "RtlTryEnterCriticalSection", rtl_try_enter_critical_section);
|
|
state.register_export(Xboxkrnl, 0x0142, "RtlUnicodeStringToAnsiString", stub_success);
|
|
state.register_export(Xboxkrnl, 0x0143, "RtlUnicodeToMultiByteN", stub_success);
|
|
- state.register_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind);
|
|
- state.register_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf);
|
|
+ // Phase C+6½: `RtlUnwind` (ord 0x147) is table-entry-only in canary
|
|
+ // — no `DECLARE_XBOXKRNL_EXPORT(RtlUnwind)`. Mirror canary's silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind);
|
|
+ // Phase C+6½: `_vsnprintf` (ord 0x14D) is table-entry-only in
|
|
+ // canary — no `DECLARE_XBOXKRNL_EXPORT(_vsnprintf)`. Mirror silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf);
|
|
|
|
// Stfs
|
|
- state.register_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success);
|
|
- state.register_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success);
|
|
+ // Phase C+6½: `StfsCreateDevice` (ord 0x259) and `StfsControlDevice`
|
|
+ // (ord 0x25A) are table-entry-only in canary. `StfsCreateDevice` is
|
|
+ // the C+6-noted driver of tid=7→tid=2 divergence at idx=15.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success);
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success);
|
|
|
|
// Video
|
|
state.register_export(Xboxkrnl, 0x01B1, "VdCallGraphicsNotificationRoutines", stub_success);
|
|
@@ -185,9 +232,11 @@ 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_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
|
|
+ state.register_void_export(Xboxkrnl, 0x0192, "XeCryptSha", xe_crypt_sha);
|
|
+ state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", xe_keys_console_private_key_sign);
|
|
+ // Phase C+6½: `XeKeysConsoleSignatureVerification` (ord 0x257) is
|
|
+ // table-entry-only in canary. Mirror silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
|
|
|
|
// Xex module
|
|
state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", xex_check_executable_privilege);
|
|
@@ -195,7 +244,9 @@ pub fn register_exports(state: &mut KernelState) {
|
|
state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address);
|
|
|
|
// Exception handling
|
|
- state.register_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler);
|
|
+ // Phase C+6½: `__C_specific_handler` (ord 0x1A5) is table-entry-only
|
|
+ // in canary. Mirror silence.
|
|
+ state.register_unimplemented_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler);
|
|
}
|
|
|
|
// ===== Generic stubs =====
|
|
@@ -375,38 +426,51 @@ fn ke_query_base_priority_thread(
|
|
ctx.gpr[3] = pri as u32 as u64;
|
|
}
|
|
|
|
-/// `KeSetIdealProcessor(thread_handle, proc_number) -> u8 old_ideal` —
|
|
-/// Axis 5. Stores the hint on the `GuestThread` for future spawn-sibling
|
|
-/// placement; does NOT migrate a live thread (use `KeSetAffinityThread`
|
|
-/// for that).
|
|
-fn ke_set_ideal_processor(
|
|
+/// Phase C+6½ hallucination fix: ord 0x82 maps to `KeQueryInterruptTime`
|
|
+/// in canary's `xboxkrnl_table.inc:130`, with a `DECLARE_XBOXKRNL_EXPORT`
|
|
+/// shim in `xboxkrnl_misc.cc:119-127`. Ours previously mis-labeled this
|
|
+/// ord as `KeQueryIdealProcessor` (a real NT function, but at a different
|
|
+/// position on Xbox 360 — not at 0x82). The hallucinated body returned
|
|
+/// the calling thread's `ideal_processor` byte; guests calling
|
|
+/// `KeQueryInterruptTime` to read the system interrupt-time counter were
|
|
+/// receiving a 1-byte processor index instead.
|
|
+///
|
|
+/// Canary returns `bundle->interrupt_time` (u64) — the monotonic system
|
|
+/// interrupt-time counter maintained by the kernel timer ISR. Ours has
|
|
+/// no `X_TIME_STAMP_BUNDLE` infrastructure, so we mirror the
|
|
+/// `KeQuerySystemTime` approach: return a fixed synthetic value that
|
|
+/// gives a plausible monotonic-looking u64. Determinism per `KernelState`
|
|
+/// requires this be reproducible — a constant satisfies both.
|
|
+fn ke_query_interrupt_time(
|
|
ctx: &mut PpcContext,
|
|
_mem: &GuestMemory,
|
|
- state: &mut KernelState,
|
|
+ _state: &mut KernelState,
|
|
) {
|
|
- let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
|
- let ideal = ctx.gpr[4] as u8;
|
|
- let prev = state
|
|
- .scheduler
|
|
- .find_by_handle(handle)
|
|
- .map(|r| state.scheduler.set_ideal_ref(r, ideal))
|
|
- .unwrap_or(0xFF);
|
|
- ctx.gpr[3] = prev as u64;
|
|
+ // Synthetic interrupt-time count. Units are 100ns ticks since boot;
|
|
+ // value chosen large enough to look post-boot but small enough that
|
|
+ // any timer-arithmetic stays in u32 range when masked. Matches the
|
|
+ // determinism pattern used by `ke_query_system_time` above.
|
|
+ const FAKE_INTERRUPT_TIME: u64 = 0x0000_0001_0000_0000;
|
|
+ ctx.gpr[3] = FAKE_INTERRUPT_TIME;
|
|
}
|
|
|
|
-fn ke_query_ideal_processor(
|
|
- ctx: &mut PpcContext,
|
|
- _mem: &GuestMemory,
|
|
- state: &mut KernelState,
|
|
-) {
|
|
- let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
|
- let ideal = state
|
|
- .scheduler
|
|
- .find_by_handle(handle)
|
|
- .and_then(|r| state.scheduler.ideal_ref(r))
|
|
- .unwrap_or(0);
|
|
- ctx.gpr[3] = ideal as u64;
|
|
-}
|
|
+/// Phase C+6½ hallucination fix: ord 0x98 maps to
|
|
+/// `KeSetBackgroundProcessors` in canary's `xboxkrnl_table.inc:166`.
|
|
+/// Canary has NO `DECLARE_XBOXKRNL_EXPORT` shim for this name — it's a
|
|
+/// table-entry-only export, routed through the syscall thunk
|
|
+/// (`xex_module.cc:1310-1335`) which is a no-op. Ours previously
|
|
+/// mis-labeled this ord as `KeSetIdealProcessor` (a real NT function but
|
|
+/// at a different position on Xbox 360) and the hallucinated body wrote
|
|
+/// to `GuestThread::ideal_processor` — a state mutation under the wrong
|
|
+/// semantic name. Guests calling `KeSetBackgroundProcessors` to mask off
|
|
+/// CPUs for background work were instead pinning the thread's ideal
|
|
+/// processor hint.
|
|
+///
|
|
+/// Replaced with a no-op (`stub_success`) registered via
|
|
+/// `register_unimplemented_export` so the Phase A emitter stays silent
|
|
+/// (matching canary's syscall-thunk path). The underlying
|
|
+/// `Scheduler::set_ideal_ref`/`ideal_ref` methods remain available for
|
|
+/// `NtSetInformationThread` info-class `ThreadIdealProcessor`.
|
|
|
|
/// `NtSetInformationThread(handle, info_class, info_ptr, info_len)` —
|
|
/// minimal Axis 5 wiring for priority / affinity / ideal-processor
|
|
@@ -453,18 +517,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 +574,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 +714,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 +946,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 +972,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 +1082,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 +1178,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 +1252,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 +1296,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 +1335,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 +1373,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 +1581,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 +1603,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 +1619,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 +2212,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 +2662,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 +3615,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`
|
|
@@ -3717,17 +4138,23 @@ fn ke_set_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState
|
|
// for why we need the lazy-shadow step here.
|
|
let h = ctx.gpr[3] as u32;
|
|
ensure_dispatcher_object(state, mem, h);
|
|
- let previous = match state.objects.get_mut(&h) {
|
|
+ // Canary parity (xevent.cc:60-64): `XEvent::Set` returns constant `1`
|
|
+ // on success, NOT the prior signaled state as the NT contract claims.
|
|
+ // We compute `previous` for internal bookkeeping (audit_signal,
|
|
+ // wake_eligible_waiters honor the prior-state read), but report
|
|
+ // `1` for success / `0` for "no dispatcher found" to match the
|
|
+ // canary Phase A oracle. See Phase C+7 investigation.md.
|
|
+ let (previous, found) = match state.objects.get_mut(&h) {
|
|
Some(KernelObject::Event { signaled, .. }) => {
|
|
let prev = *signaled;
|
|
*signaled = true;
|
|
- prev as u32
|
|
+ (prev as u32, true)
|
|
}
|
|
- _ => 0,
|
|
+ _ => (0u32, false),
|
|
};
|
|
state.audit_signal(h, ctx.lr as u32, "KeSetEvent", previous as u64);
|
|
wake_eligible_waiters(state, h);
|
|
- ctx.gpr[3] = previous as u64;
|
|
+ ctx.gpr[3] = if found { 1 } else { 0 };
|
|
}
|
|
|
|
fn ke_reset_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|
@@ -3747,18 +4174,22 @@ fn ke_reset_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelSta
|
|
fn nt_set_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|
let handle = ctx.gpr[3] as u32;
|
|
let prev_ptr = ctx.gpr[4] as u32;
|
|
- let previous = match state.objects.get_mut(&handle) {
|
|
+ // Canary parity (xboxkrnl_threading.cc:610-628): the optional out-pointer
|
|
+ // is filled with `was_signalled` = `ev->Set()` = constant 1 (see
|
|
+ // xevent.cc:60-64), NOT the prior signaled state. r3 carries
|
|
+ // STATUS_SUCCESS. We retain `previous` for internal audit/wake plumbing.
|
|
+ let (previous, found) = match state.objects.get_mut(&handle) {
|
|
Some(KernelObject::Event { signaled, .. }) => {
|
|
let prev = *signaled;
|
|
*signaled = true;
|
|
- prev as u32
|
|
+ (prev as u32, true)
|
|
}
|
|
- _ => 0,
|
|
+ _ => (0u32, false),
|
|
};
|
|
state.audit_signal(handle, ctx.lr as u32, "NtSetEvent", previous as u64);
|
|
wake_eligible_waiters(state, handle);
|
|
- if prev_ptr != 0 {
|
|
- mem.write_u32(prev_ptr, previous);
|
|
+ if prev_ptr != 0 && found {
|
|
+ mem.write_u32(prev_ptr, 1);
|
|
}
|
|
ctx.gpr[3] = STATUS_SUCCESS;
|
|
}
|
|
@@ -4423,12 +4854,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,20 +4876,95 @@ mod tests {
|
|
assert_eq!(r.hw_id, 5);
|
|
}
|
|
|
|
- /// Axis 5: `KeSetIdealProcessor` stores a hint on the thread
|
|
- /// without migrating it; query round-trips.
|
|
+ /// 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_ideal_processor_round_trips() {
|
|
+ fn ke_set_affinity_thread_null_out_ptr_still_succeeds() {
|
|
let (mut ctx, mut mem, mut state) = fresh();
|
|
- // Main thread handle is 0x1000.
|
|
- ctx.gpr[3] = 0x1000;
|
|
- ctx.gpr[4] = 3;
|
|
- ke_set_ideal_processor(&mut ctx, &mut mem, &mut state);
|
|
+ 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: scheduler-level ideal-processor hint round-trip via
|
|
+ /// `Scheduler::set_ideal_ref` / `ideal_ref`. The previous test
|
|
+ /// exercised `ke_set_ideal_processor` / `ke_query_ideal_processor`
|
|
+ /// which were hallucinated functions at the wrong ordinals — those
|
|
+ /// bodies were removed in Phase C+6½. The underlying scheduler
|
|
+ /// state still backs `NtSetInformationThread` info-class
|
|
+ /// `ThreadIdealProcessor`.
|
|
+ #[test]
|
|
+ fn scheduler_ideal_processor_round_trips() {
|
|
+ let (_, _, mut state) = fresh();
|
|
+ let r = state.scheduler.find_by_handle(0x1000).expect("main alive");
|
|
// Prior was 0xFF (unset sentinel).
|
|
- assert_eq!(ctx.gpr[3], 0xFF);
|
|
- ctx.gpr[3] = 0x1000;
|
|
- ke_query_ideal_processor(&mut ctx, &mut mem, &mut state);
|
|
- assert_eq!(ctx.gpr[3], 3);
|
|
+ let prev = state.scheduler.set_ideal_ref(r, 3);
|
|
+ assert_eq!(prev, 0xFF);
|
|
+ let queried = state.scheduler.ideal_ref(r);
|
|
+ assert_eq!(queried, Some(3));
|
|
+ }
|
|
+
|
|
+ /// Phase C+6½: `KeQueryInterruptTime` (ord 0x82) returns a
|
|
+ /// non-zero monotonic u64 in gpr[3]. Previously this ord was
|
|
+ /// mis-labeled `KeQueryIdealProcessor` and returned a 1-byte
|
|
+ /// processor index — guests querying the system interrupt-time
|
|
+ /// counter received the wrong value.
|
|
+ #[test]
|
|
+ fn ke_query_interrupt_time_returns_synthetic_u64() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ // Pre-clear gpr[3] so we know the function wrote it.
|
|
+ ctx.gpr[3] = 0;
|
|
+ ke_query_interrupt_time(&mut ctx, &mut mem, &mut state);
|
|
+ assert_ne!(ctx.gpr[3], 0, "interrupt time must be non-zero");
|
|
+ // Should be 64-bit (above u32::MAX) to ensure it's not
|
|
+ // truncated to a processor-index byte.
|
|
+ assert!(
|
|
+ ctx.gpr[3] > 0xFFFF_FFFF,
|
|
+ "interrupt time must occupy 64 bits, got {:#x}",
|
|
+ ctx.gpr[3]
|
|
+ );
|
|
}
|
|
|
|
/// Axis 5: `NtSetInformationThread` class `ThreadAffinityMask`
|
|
@@ -4660,6 +5175,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 +6818,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 +6964,367 @@ 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");
|
|
+ }
|
|
+
|
|
+ // ---------------------------------------------------------------
|
|
+ // Phase C+7 — KeSetEvent / NtSetEvent canary-parity return value
|
|
+ // ---------------------------------------------------------------
|
|
+
|
|
+ /// Canary parity: `KeSetEvent` on an unsignaled auto-reset event
|
|
+ /// must return constant `1` (NOT prior state). See investigation
|
|
+ /// for the `XEvent::Set` reference path.
|
|
+ #[test]
|
|
+ fn ke_set_event_returns_constant_one_on_unsignaled_auto_reset() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let kevent_ptr = SCRATCH_BASE + 0x900;
|
|
+ write_dispatcher_header(&mut mem, kevent_ptr, 1, 0); // auto-reset, unsignaled
|
|
+ ctx.gpr[3] = kevent_ptr as u64;
|
|
+ ke_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], 1,
|
|
+ "KeSetEvent must return constant 1 on success (canary parity, xevent.cc:60-64)"
|
|
+ );
|
|
+ // Shadow must be signaled even though the return value is constant.
|
|
+ match state.objects.get(&kevent_ptr) {
|
|
+ Some(KernelObject::Event { signaled, .. }) => assert!(*signaled),
|
|
+ _ => panic!("shadow not minted"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /// Canary parity: `KeSetEvent` on an already-signaled manual-reset
|
|
+ /// event also returns constant `1` (not prior `1`). Same constant.
|
|
+ #[test]
|
|
+ fn ke_set_event_returns_constant_one_on_already_signaled_manual_reset() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let kevent_ptr = SCRATCH_BASE + 0xA00;
|
|
+ write_dispatcher_header(&mut mem, kevent_ptr, 0, 1); // manual-reset, signaled
|
|
+ ctx.gpr[3] = kevent_ptr as u64;
|
|
+ ke_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], 1,
|
|
+ "KeSetEvent returns 1 regardless of prior state (canary parity)"
|
|
+ );
|
|
+ match state.objects.get(&kevent_ptr) {
|
|
+ Some(KernelObject::Event { signaled, .. }) => assert!(*signaled),
|
|
+ _ => panic!("shadow vanished"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /// Canary parity: `NtSetEvent` with null `PreviousState` ptr returns
|
|
+ /// STATUS_SUCCESS and performs no out-pointer write.
|
|
+ #[test]
|
|
+ fn nt_set_event_null_prev_ptr_returns_status_success_no_write() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let handle = state.alloc_handle_for(KernelObject::Event {
|
|
+ manual_reset: false,
|
|
+ signaled: false,
|
|
+ waiters: Vec::new(),
|
|
+ });
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ ctx.gpr[4] = 0; // null out-pointer
|
|
+ nt_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], STATUS_SUCCESS,
|
|
+ "NtSetEvent must return STATUS_SUCCESS"
|
|
+ );
|
|
+ // Event must be signaled.
|
|
+ match state.objects.get(&handle) {
|
|
+ Some(KernelObject::Event { signaled, .. }) => assert!(*signaled),
|
|
+ _ => panic!("handle lookup broken"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /// Canary parity: `NtSetEvent` with a valid out-pointer writes
|
|
+ /// **constant 1** (canary's `was_signalled = ev->Set()` always 1),
|
|
+ /// NOT the prior signaled state. See xboxkrnl_threading.cc:610-628.
|
|
+ #[test]
|
|
+ fn nt_set_event_valid_prev_ptr_writes_constant_one_and_returns_success() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let handle = state.alloc_handle_for(KernelObject::Event {
|
|
+ manual_reset: false,
|
|
+ signaled: false,
|
|
+ waiters: Vec::new(),
|
|
+ });
|
|
+ let prev_ptr = SCRATCH_BASE + 0xB00;
|
|
+ mem.write_u32(prev_ptr, 0xDEAD_BEEF); // sentinel — overwrite expected
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ ctx.gpr[4] = prev_ptr as u64;
|
|
+ nt_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(
|
|
+ ctx.gpr[3], STATUS_SUCCESS,
|
|
+ "NtSetEvent must return STATUS_SUCCESS"
|
|
+ );
|
|
+ assert_eq!(
|
|
+ mem.read_u32(prev_ptr),
|
|
+ 1,
|
|
+ "PreviousState out-ptr must receive constant 1 (canary parity)"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ /// Canary parity: `NtSetEvent` on an already-signaled event still
|
|
+ /// writes constant `1` to the out-pointer (not the prior `1`,
|
|
+ /// though they happen to match here — distinguished from the
|
|
+ /// prior-state-write bug by the auto-reset/un-signaled case above).
|
|
+ #[test]
|
|
+ fn nt_set_event_on_signaled_event_writes_one() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let handle = state.alloc_handle_for(KernelObject::Event {
|
|
+ manual_reset: true,
|
|
+ signaled: true,
|
|
+ waiters: Vec::new(),
|
|
+ });
|
|
+ let prev_ptr = SCRATCH_BASE + 0xC00;
|
|
+ mem.write_u32(prev_ptr, 0);
|
|
+ ctx.gpr[3] = handle as u64;
|
|
+ ctx.gpr[4] = prev_ptr as u64;
|
|
+ nt_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(mem.read_u32(prev_ptr), 1);
|
|
+ // Event stays signaled (manual-reset).
|
|
+ match state.objects.get(&handle) {
|
|
+ Some(KernelObject::Event { signaled, .. }) => assert!(*signaled),
|
|
+ _ => panic!("handle lookup broken"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /// Wake-cascade regression: KeSetEvent on a manual-reset event with
|
|
+ /// a parked waiter still wakes the waiter post-fix. The return-value
|
|
+ /// change is observation-only — internal wake plumbing uses the
|
|
+ /// `previous` read, not the return value.
|
|
+ #[test]
|
|
+ fn ke_set_event_post_fix_still_wakes_waiter() {
|
|
+ let (mut ctx, mut mem, mut state) = fresh();
|
|
+ let kevent_ptr = SCRATCH_BASE + 0xD00;
|
|
+ write_dispatcher_header(&mut mem, kevent_ptr, 0, 0); // manual-reset, unsignaled
|
|
+ // Mint the shadow first by calling reset_event (no waiter yet).
|
|
+ ctx.gpr[3] = kevent_ptr as u64;
|
|
+ ke_reset_event(&mut ctx, &mut mem, &mut state);
|
|
+ // Park a fake waiter.
|
|
+ match state.objects.get_mut(&kevent_ptr) {
|
|
+ Some(KernelObject::Event { waiters, .. }) => {
|
|
+ waiters.push(ThreadRef { hw_id: 4, idx: 0, generation: 0 });
|
|
+ }
|
|
+ _ => panic!("shadow not minted"),
|
|
+ }
|
|
+ // Signal.
|
|
+ ctx.gpr[3] = kevent_ptr as u64;
|
|
+ ke_set_event(&mut ctx, &mut mem, &mut state);
|
|
+ assert_eq!(ctx.gpr[3], 1, "constant 1 return preserved");
|
|
+ // Manual-reset: waiter list drained after wake.
|
|
+ match state.objects.get(&kevent_ptr) {
|
|
+ Some(KernelObject::Event { signaled, waiters, .. }) => {
|
|
+ assert!(*signaled, "manual-reset stays signaled");
|
|
+ assert!(waiters.is_empty(), "manual-reset wake drains all waiters");
|
|
+ }
|
|
+ _ => panic!("shadow vanished"),
|
|
+ }
|
|
+ }
|
|
}
|