fix(kernel): KRNBUG-IO-002 — vol-info class-3 returns 0x10000 alloc unit (canary NullDevice)
`nt_query_volume_information_file` class-3 (`FileFsSizeInformation`) was returning sectors_per_unit=1, bytes_per_sector=2048 (alloc unit 2048). Replaced with canary's NullDevice byte-identical values sectors=0x80, bps=0x200 (alloc unit 0x10000), with total / available allocation units lowered to 0x10 / 0x10 to match. Reference: xenia-canary/src/xenia/vfs/devices/null_device.h:38-46 (`NullDevice::sectors_per_allocation_unit()` and `bytes_per_sector()`); consumed by canary's `NtQueryVolumeInformationFile_entry` at xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc:355-365. Tests 591 → 592 (added `nt_query_volume_information_file_class3_returns_64k_alloc_unit`). Lockstep `instructions=100000010, swaps=2, draws=0` deterministic across two `--stable-digest -n 100M` reruns. sylpheed_n50m oracle still matches its existing golden — observably a no-op at -n 50M. The audit-006-predicted 7→0 cascade did NOT fire (canary-only exports still 7, identical set; XexCheckExecutablePrivilege still priv=0xA only; XamTaskSchedule still 0). All 16 NtQueryVolumeInformationFile calls in our 500M trace originate from a single LR 0x82611f38 and complete successfully — vol-info is therefore not the priv-11 gate. The fix value is correct (canary-byte-identical) but is not load-bearing for the gate; landing it anyway because it's the right value and unblocks no regression. Stop condition triggered per the IO-002 task brief — no second fix this session. Next-session: --pc-probe on sub_824A9710 entry to find the actual upstream gate. See `audit-findings.md` (KRNBUG-IO-002 entry) and `audit-runs/post-IO-002/` for the full diagnostic trail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4999,3 +4999,83 @@ for n in sorted(set(canary) - set(ours)):
|
|||||||
print(f' {canary[n]:>5} {n}')
|
print(f' {canary[n]:>5} {n}')
|
||||||
PY
|
PY
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### KRNBUG-IO-002 — `nt_query_volume_information_file` block size (LANDED, gate hypothesis FALSIFIED)
|
||||||
|
|
||||||
|
**Status:** applied (branch `xboxkrnl-vol-allocunit/p0-65536-cluster`,
|
||||||
|
single squash commit). Tests 591 → 592. Lockstep
|
||||||
|
`instructions=100000010, swaps=2, draws=0` deterministic across two
|
||||||
|
reruns. sylpheed_n50m oracle still matches its existing golden.
|
||||||
|
|
||||||
|
**The fix.** `crates/xenia-kernel/src/exports.rs:1241-1269`,
|
||||||
|
`nt_query_volume_information_file` class-3 (FileFsSizeInformation)
|
||||||
|
branch, was returning `(total=0x100000, free=0,
|
||||||
|
sectors_per_unit=1, bytes_per_sector=2048)`. Replaced with the
|
||||||
|
canary-NullDevice byte-identical `(total=0x10, free=0x10,
|
||||||
|
sectors_per_unit=0x80, bytes_per_sector=0x200)` (product = 65536).
|
||||||
|
Reference: `xenia-canary/src/xenia/vfs/devices/null_device.h:38-46`.
|
||||||
|
|
||||||
|
**The cascade hypothesis.** AUDIT-006 predicted that fixing this would
|
||||||
|
unblock seven canary-only kernel exports — the priv-11 query at
|
||||||
|
`sub_824A9710` would fire, `XamTaskSchedule` at `lr=0x824a9a10` would
|
||||||
|
fire, the Cache0 callback thread would spawn, and dispatcher 0x100c's
|
||||||
|
producer would finally fire (closing the 6-session producer hunt).
|
||||||
|
|
||||||
|
**The cascade DID NOT FIRE.** Fresh 500 M trace at
|
||||||
|
`audit-runs/post-IO-002/ours.log` (692 MB, 5.6 M lines):
|
||||||
|
|
||||||
|
| Metric | Pre-IO-002 (audit-006) | Post-IO-002 |
|
||||||
|
|---|---|---|
|
||||||
|
| canary-only kernel exports | 7 | **7 (identical set)** |
|
||||||
|
| `XexCheckExecutablePrivilege` calls | 1 (priv=0xA only) | **1** (still no priv=0xB) |
|
||||||
|
| `XamTaskSchedule` calls | 0 | **0** |
|
||||||
|
| `KeResetEvent / ObCreateSymbolicLink / KeReleaseSemaphore / ExTerminateThread / XamTaskCloseHandle / XamUserReadProfileSettings` | 0 | **0** |
|
||||||
|
| `NtQueryVolumeInformationFile` calls | 16 | **16** (no new sites reached) |
|
||||||
|
| `swaps` | 2 | 2 |
|
||||||
|
| `draws` | 0 | 0 |
|
||||||
|
| Worker thread spawns | 19 | 18 (within noise) |
|
||||||
|
| `imports` at -n 100M (stable digest) | 987686 | **987630** (-56) |
|
||||||
|
|
||||||
|
**Diagnostic.** All 16 `NtQueryVolumeInformationFile` calls in our trace
|
||||||
|
originate from a single LR `0x82611f38`, a downstream consumer that
|
||||||
|
**completes successfully** in both pre- and post-fix runs. The audit-006
|
||||||
|
premise that `sub_824ABA98`/`sub_824A9710` consume the volume-info reply
|
||||||
|
at the priv-11 gate is therefore likely incorrect, *or* the gate consumes
|
||||||
|
a different information class via a different export entirely.
|
||||||
|
|
||||||
|
**Stop-condition triggered.** Per the IO-002 task brief, this session
|
||||||
|
landed the correct fix (it makes our reply byte-identical to canary's
|
||||||
|
NullDevice and survives every test we have) but did not pivot to a
|
||||||
|
second fix. The branch is kept because the value change is correct
|
||||||
|
and unblocks no regression; it is, however, **not load-bearing for
|
||||||
|
the priv-11 gate**.
|
||||||
|
|
||||||
|
**Next-session next-gate hypothesis (untested, ranked by likelihood):**
|
||||||
|
|
||||||
|
1. **`sub_824A9710` early-exit probe.** Per AUDIT-005 instrumentation
|
||||||
|
the priv-11 site has never fired in any session. Use `--pc-probe` on
|
||||||
|
the entry of `sub_824A9710` and probe each conditional branch within
|
||||||
|
it; whichever branch exits the function before the priv-11
|
||||||
|
`XexCheckExecutablePrivilege` call site is the actual gate.
|
||||||
|
2. **Different info-class.** `nt_query_information_file` (class 5
|
||||||
|
`FileStandardInformation`, class 22 etc.) or
|
||||||
|
`nt_query_full_attributes_file` may be the actual consumer. The
|
||||||
|
16 calls at LR `0x82611f38` are *not* the gate even though they
|
||||||
|
complete successfully.
|
||||||
|
3. **Mis-attributed disasm.** AUDIT-005's identification of
|
||||||
|
`sub_824ABA98 = VerifyDirBlockSize` came from disasm reading; IO-001's
|
||||||
|
runtime trace already invalidated parts of that attribution.
|
||||||
|
Re-disassemble `sub_824A9710` with `xenia-rs dis --json --at 0x824a9710`
|
||||||
|
and walk every conditional that might exit before the priv-11 query.
|
||||||
|
4. **A different IOCTL.** `NtDeviceIoControlFile` is now reachable
|
||||||
|
(KRNBUG-IO-001 unblocked it); some FsCtl response we return may be
|
||||||
|
the new gate.
|
||||||
|
|
||||||
|
**Trace artifacts:**
|
||||||
|
- `audit-runs/post-IO-002/ours.log` — 500 M trace, post-fix
|
||||||
|
- `audit-runs/post-IO-002/canary.log` — copy of the audit-006 canary oracle
|
||||||
|
- `audit-runs/post-IO-002/diff.py` — copy of audit-006 diff tool
|
||||||
|
- `audit-runs/post-IO-002/lock_n100m_run{1,2}.json` — bit-identical lockstep digests
|
||||||
|
- `audit-runs/post-IO-002/canary_only.txt` — set-difference output (the 7-entry list)
|
||||||
|
- `audit-runs/post-IO-002/canary_exports.txt`, `ours_exports.txt` — sorted unique export names
|
||||||
|
|
||||||
|
|||||||
@@ -1250,10 +1250,10 @@ fn nt_query_volume_information_file(ctx: &mut PpcContext, mem: &GuestMemory, _st
|
|||||||
// SectorsPerAllocationUnit(u32), BytesPerSector(u32)
|
// SectorsPerAllocationUnit(u32), BytesPerSector(u32)
|
||||||
let written: u32 = match class {
|
let written: u32 = match class {
|
||||||
3 if length >= 24 => {
|
3 if length >= 24 => {
|
||||||
mem.write_u64(info, 0x10_0000); // ~2GB at 2KB sectors
|
mem.write_u64(info, 0x10);
|
||||||
mem.write_u64(info + 8, 0);
|
mem.write_u64(info + 8, 0x10);
|
||||||
mem.write_u32(info + 16, 1);
|
mem.write_u32(info + 16, 0x80);
|
||||||
mem.write_u32(info + 20, 2048);
|
mem.write_u32(info + 20, 0x200);
|
||||||
24
|
24
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -4331,6 +4331,37 @@ mod tests {
|
|||||||
assert_eq!(mem.read_u8(info_buf + 21), 0, "normal file not directory");
|
assert_eq!(mem.read_u8(info_buf + 21), 0, "normal file not directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nt_query_volume_information_file_class3_returns_64k_alloc_unit() {
|
||||||
|
let (mut ctx, mut mem, mut state) = fresh();
|
||||||
|
let h = state.alloc_handle_for(KernelObject::File {
|
||||||
|
path: String::new(),
|
||||||
|
size: 0,
|
||||||
|
position: 0,
|
||||||
|
data: std::sync::Arc::new(Vec::new()),
|
||||||
|
dir_enum_pos: None,
|
||||||
|
});
|
||||||
|
let iosb = SCRATCH_BASE;
|
||||||
|
let info_buf = SCRATCH_BASE + 0x100;
|
||||||
|
ctx.gpr[3] = h as u64;
|
||||||
|
ctx.gpr[4] = iosb as u64;
|
||||||
|
ctx.gpr[5] = info_buf as u64;
|
||||||
|
ctx.gpr[6] = 24;
|
||||||
|
ctx.gpr[7] = 3; // FileFsSizeInformation
|
||||||
|
nt_query_volume_information_file(&mut ctx, &mut mem, &mut state);
|
||||||
|
|
||||||
|
assert_eq!(ctx.gpr[3], STATUS_SUCCESS as u64);
|
||||||
|
let sectors_per_unit = mem.read_u32(info_buf + 16);
|
||||||
|
let bytes_per_sector = mem.read_u32(info_buf + 20);
|
||||||
|
assert_eq!(sectors_per_unit, 0x80);
|
||||||
|
assert_eq!(bytes_per_sector, 0x200);
|
||||||
|
assert_eq!(
|
||||||
|
sectors_per_unit * bytes_per_sector,
|
||||||
|
0x10000,
|
||||||
|
"alloc unit must be 64 KiB to match canary NullDevice",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== PKEVENT shim =====
|
// ===== PKEVENT shim =====
|
||||||
|
|
||||||
/// Write a DISPATCHER_HEADER at the given guest pointer.
|
/// Write a DISPATCHER_HEADER at the given guest pointer.
|
||||||
|
|||||||
Reference in New Issue
Block a user