# Phase C+13 investigation — NtCreateFile `game:\dat\files.tbl` ## Predecessor state (from Phase C+12) Cold-vs-cold first divergence at `tid_event_idx=103862` on the canary tid=6 → ours tid=1 main chain: ``` canary: NtCreateFile(path="game:\\dat\\files.tbl") -> 0xc0000034 (STATUS_OBJECT_NAME_NOT_FOUND) ours: NtCreateFile(path="game:\\dat\\files.tbl") -> 0x00000000 (STATUS_SUCCESS, synth-empty stub) ``` Verified from the C+12 archived JSONLs: - `xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/ours-cold.jsonl` events 103860-103862: import.call → kernel.call → kernel.return = 0. - `xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/canary-cold-tid6-250k.jsonl` events 103860-103862: same import.call → kernel.call → kernel.return = `0xc0000034`. Canary's next event (idx 103863): `RtlNtStatusToDosError(0xc0000034) -> 2` (ERROR_FILE_NOT_FOUND). Sylpheed has proper NOT_FOUND handling and continues; the synth-empty was masking the correct branch. ## Canary semantics (xboxkrnl_io.cc:39-111) `NtCreateFile_entry` is a thin wrapper: 1. Translate the `OBJECT_ATTRIBUTES.name` ANSI_STRING to a path. 2. Path validation: `IsValidPath` → `STATUS_OBJECT_NAME_INVALID` on reject. 3. Resolve the `root_directory` handle if non-zero. 4. Call `kernel_state()->file_system()->OpenFile(...)`. 5. **Return the `OpenFile` status verbatim.** If the file isn't present in any mounted device, that's `STATUS_OBJECT_NAME_NOT_FOUND` (`VirtualFileSystem::ResolvePath` returns nullptr for unresolved paths, which `OpenFile` translates to NOT_FOUND). 6. On success: alloc handle and write to `handle_out`. For `game:\dat\files.tbl` specifically: the `game:` symlink points to the disc device. The disc image ISO doesn't contain `dat/files.tbl` (disc dump omission — likely a region-specific or build-time-only file). `OpenFile` returns NOT_FOUND, the function returns NOT_FOUND. ## Ours pre-fix (exports.rs `open_vfs_file`) `open_vfs_file` had three paths: 1. **Empty path** (root-of-device) → synth-empty handle (special-cased for `NtCreateFile("game:\")` disc-validation probe). 2. **`cache:` prefix** → host-FS-backed via `open_cache_file` (AUDIT-038). 3. **Otherwise**: call `vfs.read_file(&path)`. On `Ok(bytes)` return handle. **On `Err(e)`: synth a zero-byte file and return SUCCESS.** The (3) `Err` branch was the synth-empty fallback. Its docstring acknowledged two rationales: > 1. Writable system partitions (`cache:`, `partition0:`, `partition1:`) > aren't backed by the disc — we synth so opens succeed. > 2. Disc files that didn't make it into the ISO rip — returning > NOT_FOUND would make Sylpheed's boot validator call > `XamShowDirtyDiscErrorUI` → dashboard exit. But rationale (1) was already covered by the AUDIT-038 `cache:` short-circuit, and rationale (2) is empirically WRONG: canary returns NOT_FOUND for `game:\dat\files.tbl` and Sylpheed continues — there is no `XamShowDirtyDiscErrorUI` event in canary's trace anywhere near idx 103862. The synth-empty was misleading the boot trajectory in a non-canary direction. ## Fix shape Two surgical changes: 1. New helper `is_disc_prefix(raw_path: &str) -> bool` that recognises `game:\`, `d:\`/`D:\`, and `\Device\Cdrom0\` (the subset of `path::DEVICE_PREFIXES` that resolves to the read-only disc). 2. `open_vfs_file` captures the raw path via `crate::path::object_attributes_raw_name` (alongside the existing normalised `path`). In the `Err(e)` arm, if `is_disc_prefix(&raw_path)`, return `STATUS_OBJECT_NAME_NOT_FOUND` (mirroring canary). Otherwise keep the legacy synth-empty. The `cache:` short-circuit is upstream of this branch and unchanged; its existing NOT_FOUND-on-FILE_OPEN-miss behaviour is preserved. ## Tests - `is_disc_prefix_recognises_disc_aliases` (12 assertions, mix of disc and non-disc prefixes including case variants). - `nt_create_file_game_prefix_missing_returns_not_found` — primary. - `nt_create_file_cdrom_prefix_missing_returns_not_found` — `\Device\Cdrom0\` alias. - `nt_create_file_non_disc_prefix_missing_still_synthesizes` — regression guard for `\Device\Harddisk0\Partition1\sys.bin`. ## Risk analysis (pre-fix) - **Sylpheed might crash on NOT_FOUND**: REFUTED. Canary returns NOT_FOUND, then Sylpheed calls `RtlNtStatusToDosError` and continues; +712 events of further progress observed in cold-vs-cold (no fault, no `XamShowDirtyDiscErrorUI`). - **Reading-error #23 regression**: matched-prefix MUST grow, not shrink. Cold-vs-cold confirms 103862 → 104574 (+712); no regression. - **Sister-chain regression**: confirmed unchanged for all 5 sister chains (see `cold-vs-cold-result.md`). - **Stable digest shift**: expected; the synth-empty was emitting one extra kernel.call/return pair (synth-success path); removing it for disc paths drops `imports` 40447 → 40390 (-57). Phase B `image_loaded_sha256` is unchanged.