fix(kernel): KRNBUG-IO-003 — NtDeviceIoControlFile real impl mirroring NullDevice::IoControl
Replace the stub_success registration of NtDeviceIoControlFile at
exports.rs:90 with a real handler for FsCtlCodes 0x70000 (drive
geometry) and 0x74004 (partition info), mirroring xenia-canary
xboxkrnl_io.cc:645-678 + null_device.{h,cc}. The 16-byte 0x74004
response with cache_size=0xFF000 at OUT+8 is the gate that lets
sub_824ABD88 return SUCCESS and sub_824A9710 reach the priv-11
XexCheckExecutablePrivilege site identified by KRNBUG-AUDIT-007.
Stack args 9-10 (OutputBuffer, OutputBufferLength) read from the
caller's parameter save area at [sp+0x54] / [sp+0x5C] per the Xbox
360 PowerPC EABI (linkage area sp+0..sp+8, 8-quadword spill area
sp+0x14..sp+0x54, then stack args every 8 bytes). First HLE export
in the codebase to need 9+ args.
Cascade vs. KRNBUG-AUDIT-007 prediction (5/8 held):
- XexCheckExecutablePrivilege count 1 → 2 (priv=0xA + priv=0xB) ✓
- XamTaskSchedule count 0 → 1 ✓
- canary-only exports 7 → 3 (audit predicted ≤3) ✓
- 0x15e0 semaphore signal_attempts 0 → 1 (bonus)
- 0x100c worker spawn DID NOT fire (still UNCREATED) ✗
- 0x1004 signal_attempts unchanged ✗
- Worker spawn count unchanged at 19 ✗
Tests: 592 → 594. Lockstep deterministic at -n 100M (run1 ≡ run2 ≡
run3, byte-identical). instructions=100000010 → 100000019, imports
407417 → 987524 (+2.4×). swaps=2 draws=0 plateau persists.
sylpheed_n50m golden re-baselined instructions=50000004→50000003,
imports=407362→407255. sylpheed_n2m unchanged.
Still canary-only after this fix: ExTerminateThread,
KeReleaseSemaphore, XamUserReadProfileSettings. The next downstream
gate is somewhere past XamTaskSchedule's completion path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,7 +87,7 @@ pub fn register_exports(state: &mut KernelState) {
|
||||
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
|
||||
state.register_export(Xboxkrnl, 0xD5, "NtCreateSemaphore", nt_create_semaphore);
|
||||
state.register_export(Xboxkrnl, 0xD7, "NtCreateTimer", nt_create_timer);
|
||||
state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", nt_device_io_control_file);
|
||||
state.register_export(Xboxkrnl, 0xDA, "NtDuplicateObject", nt_duplicate_object);
|
||||
state.register_export(Xboxkrnl, 0xDB, "NtFlushBuffersFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xDC, "NtFreeVirtualMemory", stub_success);
|
||||
@@ -1001,6 +1001,64 @@ fn nt_write_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelStat
|
||||
signal_io_completion_event(state, event_handle);
|
||||
}
|
||||
|
||||
/// Mirrors canary `NullDevice::IoControl` via
|
||||
/// [xboxkrnl_io.cc:645-678](xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc).
|
||||
/// Used by `XMountUtilityDrive` cache-mount probes; Sylpheed issues both
|
||||
/// `0x70000` (drive geometry) and `0x74004` (partition info) inside
|
||||
/// `sub_824ABD88`. The OUT-buffer fields it writes are the gate that
|
||||
/// keeps `sub_824A9710` from synthesizing `STATUS_OBJECT_NAME_NOT_FOUND`.
|
||||
fn nt_device_io_control_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
const X_IOCTL_DISK_GET_DRIVE_GEOMETRY: u32 = 0x70000;
|
||||
const X_IOCTL_DISK_GET_PARTITION_INFO: u32 = 0x74004;
|
||||
const STATUS_BUFFER_TOO_SMALL: u64 = 0xC000_0023;
|
||||
const STATUS_INVALID_PARAMETER: u64 = 0xC000_000D;
|
||||
const CACHE_SIZE: u64 = 0xFF000;
|
||||
|
||||
let event_handle = ctx.gpr[4] as u32;
|
||||
let io_status_block = ctx.gpr[7] as u32;
|
||||
let io_control_code = ctx.gpr[8] as u32;
|
||||
let sp = ctx.gpr[1] as u32;
|
||||
let output_buffer = mem.read_u32(sp + 0x54);
|
||||
let output_buffer_len = mem.read_u32(sp + 0x5C);
|
||||
|
||||
let status: u64 = match io_control_code {
|
||||
X_IOCTL_DISK_GET_DRIVE_GEOMETRY => {
|
||||
if output_buffer_len < 0x8 {
|
||||
STATUS_BUFFER_TOO_SMALL
|
||||
} else {
|
||||
mem.write_u32(output_buffer, (CACHE_SIZE / 512) as u32);
|
||||
mem.write_u32(output_buffer + 4, 512);
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
}
|
||||
X_IOCTL_DISK_GET_PARTITION_INFO => {
|
||||
if output_buffer_len < 0x10 {
|
||||
STATUS_BUFFER_TOO_SMALL
|
||||
} else {
|
||||
mem.write_u64(output_buffer, 0);
|
||||
mem.write_u64(output_buffer + 8, CACHE_SIZE);
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
io_control_code = format!("0x{:X}", io_control_code),
|
||||
"NtDeviceIoControlFile: unhandled IOCTL"
|
||||
);
|
||||
STATUS_INVALID_PARAMETER
|
||||
}
|
||||
};
|
||||
|
||||
let info = if status == STATUS_SUCCESS {
|
||||
output_buffer_len
|
||||
} else {
|
||||
0
|
||||
};
|
||||
write_io_status_block(mem, io_status_block, status as u32, info);
|
||||
ctx.gpr[3] = status;
|
||||
signal_io_completion_event(state, event_handle);
|
||||
}
|
||||
|
||||
/// Minimal `NtQueryInformationFile`. The only classes Sylpheed (and most
|
||||
/// games) use are `FileStandardInformation` (5) and `FilePositionInformation`
|
||||
/// (14). Anything else gets zeros + success.
|
||||
@@ -5461,6 +5519,55 @@ mod tests {
|
||||
assert!(state.xaudio.get(index).is_none());
|
||||
}
|
||||
|
||||
/// FsCtlCode 0x70000 (drive geometry): canary writes
|
||||
/// `cache_size/512` at OUT+0 and `512` at OUT+4 (both u32 BE).
|
||||
#[test]
|
||||
fn nt_device_io_control_file_drive_geometry() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
let sp = SCRATCH_BASE + 0x800;
|
||||
let iosb = SCRATCH_BASE + 0x100;
|
||||
let out_buf = SCRATCH_BASE + 0x200;
|
||||
ctx.gpr[1] = sp as u64;
|
||||
ctx.gpr[3] = 0xF800_0010;
|
||||
ctx.gpr[4] = 0;
|
||||
ctx.gpr[7] = iosb as u64;
|
||||
ctx.gpr[8] = 0x70000;
|
||||
mem.write_u32(sp + 0x54, out_buf);
|
||||
mem.write_u32(sp + 0x5C, 0x8);
|
||||
nt_device_io_control_file(&mut ctx, &mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||||
assert_eq!(mem.read_u32(out_buf), 0xFF000 / 512);
|
||||
assert_eq!(mem.read_u32(out_buf + 4), 512);
|
||||
assert_eq!(mem.read_u32(iosb), STATUS_SUCCESS as u32);
|
||||
assert_eq!(mem.read_u32(iosb + 4), 0x8);
|
||||
}
|
||||
|
||||
/// FsCtlCode 0x74004 (partition info): canary writes `0` at OUT+0 and
|
||||
/// `cache_size = 0xFF000` at OUT+8 (both u64 BE). Sub_824ABD88 at
|
||||
/// `0x824abe9c` reads OUT+8 and synthesizes `0xC0000034` if zero — a
|
||||
/// non-zero value at OUT+8 is the entire fix.
|
||||
#[test]
|
||||
fn nt_device_io_control_file_partition_info_unblocks_gate() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
let sp = SCRATCH_BASE + 0x800;
|
||||
let iosb = SCRATCH_BASE + 0x100;
|
||||
let out_buf = SCRATCH_BASE + 0x200;
|
||||
ctx.gpr[1] = sp as u64;
|
||||
ctx.gpr[3] = 0xF800_0010;
|
||||
ctx.gpr[4] = 0;
|
||||
ctx.gpr[7] = iosb as u64;
|
||||
ctx.gpr[8] = 0x74004;
|
||||
mem.write_u32(sp + 0x54, out_buf);
|
||||
mem.write_u32(sp + 0x5C, 0x10);
|
||||
nt_device_io_control_file(&mut ctx, &mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||||
assert_eq!(mem.read_u64(out_buf), 0);
|
||||
assert_eq!(mem.read_u64(out_buf + 8), 0xFF000);
|
||||
assert_ne!(mem.read_u64(out_buf + 8), 0, "OUT+8 must be non-zero");
|
||||
assert_eq!(mem.read_u32(iosb), STATUS_SUCCESS as u32);
|
||||
assert_eq!(mem.read_u32(iosb + 4), 0x10);
|
||||
}
|
||||
|
||||
/// Once a client is registered, the lockstep ticker eventually queues
|
||||
/// a fire — proves the producer pipeline is wired end-to-end through
|
||||
/// the kernel state.
|
||||
|
||||
Reference in New Issue
Block a user