From bef9793aec158e8e0ce0c1cbef7aae03281bb1a0 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 4 May 2026 20:20:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(kernel):=20KRNBUG-IO-001=20=E2=80=94=20NtR?= =?UTF-8?q?eadFile=20on=20synth-empty=20file=20returns=20SUCCESS+0,=20not?= =?UTF-8?q?=20EOF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT-005's static attribution to sub_824ABA98 was wrong. The 0xC0000011 (STATUS_END_OF_FILE) at lr=0x824a97e4 traces to the NtReadFile call at 0x824a9810 inside sub_824A9710 — the cache-loader reads 1024 B from offset 2048 of `\Device\Harddisk0\partition0`. Our synth-empty fallback returned EOF (start_pos 2048 > size 0), so the function bailed via RtlNtStatusToDosError before sub_824ABA98 was ever called. Canary mounts partition0 to a NullDevice; `NullFile::ReadSync` ([null_file.cc:24-31](xenia-canary/src/xenia/vfs/devices/null_file.cc)) returns X_STATUS_SUCCESS with bytes_read=0 and never touches the buffer. Sylpheed's caller pre-zeroes the 1024-byte stack buffer (`memset(sp+208, 0, 1024)` at sub_824A9710 prologue), validates a "Josh" magic on the first read, and falls back to the cache-recreate path when the magic doesn't match. The fix mirrors NullFile semantics: when the open synthesized a zero-length file (`data.is_empty() && size == 0`), NtReadFile returns SUCCESS with information=0 and the buffer untouched. Effects (chain-of-effects verification at -n 500M): - tests: 590 → 591 (added regression covering NullDevice semantics) - lockstep: deterministic across 3 reruns (same instructions=100000010, swaps=2) - sylpheed_n50m golden re-baselined: instructions 50000004→50000000, imports 407416→407362 - canary kernel-call diff: 10 → 7 missing exports (XeCryptSha + XeKeysConsolePrivateKeySign + NtDeviceIoControlFile now run; the cache-recreate path executes through to NtWriteFile) - boot reaches silph::Silph::Impl::OnInit: 19 worker threads spawn (was 6 before the fix) - parked-handle 0x1004 still signal_attempts=0; the original 0x100c and 0x15e0 are now because cascade walked past them and the handle assignments shifted; new parked sites: 0x12fc/0x1600/ 0x1040/0x10b8/0x15e8/0x1014/0x101c/0x10bc/0x1044 - draws=0 plateau persists; renderer is multi-causal blocked Next blocker: per the canary-only diff, XamTaskSchedule + the cluster of XAM exports (XamTaskCloseHandle, XamUserReadProfileSettings, ObCreateSymbolicLink) and the post-thread-exit chain (ExTerminateThread, KeReleaseSemaphore, KeResetEvent) are the next-up frontier. --- .../xenia-app/tests/golden/sylpheed_n50m.json | 4 +- crates/xenia-kernel/src/exports.rs | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/xenia-app/tests/golden/sylpheed_n50m.json b/crates/xenia-app/tests/golden/sylpheed_n50m.json index e1ef545..4591f81 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n50m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n50m.json @@ -1,6 +1,6 @@ { - "instructions": 50000004, - "imports": 407416, + "instructions": 50000000, + "imports": 407362, "unimpl": 0, "draws": 0, "swaps": 2, diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index a6baf43..2e77fb0 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -943,6 +943,23 @@ fn nt_read_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState }; let total = *size; + + // Synthesized empty files (system partition opens like + // `\Device\Harddisk0\partition0` that miss the disc-VFS) act as + // NullDevice handles. Canary's `NullFile::ReadSync` returns + // `X_STATUS_SUCCESS` with `bytes_read=0` and never touches the buffer + // ([null_file.cc:24-31](xenia-canary/src/xenia/vfs/devices/null_file.cc)); + // Sylpheed's cache loader at `sub_824A9710` reads 1024 B from offset + // 2048, expects success, then validates a `"Josh"` magic — falling back + // to the recreate path if the buffer (already zeroed by the caller via + // `memset(sp+208, 0, 1024)`) doesn't match. + if data.is_empty() && total == 0 { + write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, 0); + ctx.gpr[3] = STATUS_SUCCESS; + signal_io_completion_event(state, event_handle); + return; + } + if start_pos >= total { write_io_status_block(mem, io_status_block, STATUS_END_OF_FILE as u32, 0); ctx.gpr[3] = STATUS_END_OF_FILE; @@ -3980,6 +3997,44 @@ mod tests { assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS expected with null event"); } + /// Synthesized empty files (system-partition opens like + /// `\Device\Harddisk0\partition0` that miss the disc-VFS) act as + /// canary's `NullDevice`: any `NtReadFile` returns `STATUS_SUCCESS` + /// with `information=0` and the buffer untouched. Sylpheed's + /// cache-loader at `sub_824A9710` reads 1024 B from offset 2048 then + /// validates a `"Josh"` magic — falling back to the recreate path + /// when the (caller-zeroed) buffer doesn't match. + #[test] + fn nt_read_file_synth_empty_file_returns_success_with_zero_bytes() { + let (mut ctx, mut mem, mut state) = fresh(); + let synth = make_file(&mut state, Vec::new()); + // Pre-fill the buffer with a sentinel; canary's NullDevice never + // touches it, so the post-read bytes must be unchanged. + let buf: u32 = 0x4000_0100; + for i in 0..16u32 { + mem.write_u8(buf + i, 0xAB); + } + let evt = make_event(&mut state); + // Read 1024 B from offset 2048 — exactly the cache-catalog read. + let offset_ptr: u32 = 0x4000_0080; + mem.write_u64(offset_ptr, 2048); + ctx.gpr[3] = synth as u64; + ctx.gpr[4] = evt as u64; + ctx.gpr[7] = 0x4000_0000; + ctx.gpr[8] = buf as u64; + ctx.gpr[9] = 1024; + ctx.gpr[10] = offset_ptr as u64; + nt_read_file(&mut ctx, &mut mem, &mut state); + assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS for synth-empty read"); + for i in 0..16u32 { + assert_eq!(mem.read_u8(buf + i), 0xAB, "buffer at +{} must be untouched", i); + } + // IOSB.information must be 0 (matches NullFile bytes_read). + assert_eq!(mem.read_u32(0x4000_0000), 0, "iosb.status = 0"); + assert_eq!(mem.read_u32(0x4000_0004), 0, "iosb.information = 0"); + assert!(event_signaled(&state, evt), "synth-empty read must signal completion"); + } + #[test] fn nt_write_file_signals_completion_event() { let (mut ctx, mut mem, mut state) = fresh();