# Phase C+12 — investigation (canary's NtQueryFullAttributesFile) ## Canary's verified behavior ### Entry point `NtQueryFullAttributesFile_entry` lives in `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:474-513`. It takes `(X_OBJECT_ATTRIBUTES*, X_FILE_NETWORK_OPEN_INFORMATION*)` and: 1. Translates the ANSI object name to a guest VFS path (`util::TranslateAnsiPath`). 2. Calls `kernel_state()->file_system()->ResolvePath(target_path)`. 3. On hit, populates the OUT struct from `entry->{create,access, write}_timestamp()` + `entry->allocation_size()` + `entry->size()` + `entry->attributes()`. Returns `X_STATUS_SUCCESS`. 4. On miss, returns `X_STATUS_NO_SUCH_FILE` (NOT `OBJECT_NAME_NOT_FOUND`). `xboxkrnl_io.cc:498-512`: ```cpp auto entry = kernel_state()->file_system()->ResolvePath(target_path); if (entry) { file_info->creation_time = entry->create_timestamp(); file_info->last_access_time= entry->access_timestamp(); file_info->last_write_time = entry->write_timestamp(); file_info->change_time = entry->write_timestamp(); // <-- same as write file_info->allocation_size = entry->allocation_size(); file_info->end_of_file = entry->size(); file_info->attributes = entry->attributes(); return X_STATUS_SUCCESS; } return X_STATUS_NO_SUCH_FILE; ``` ### Resolution mechanism `VirtualFileSystem::ResolvePath` (`xenia-canary/src/xenia/vfs/virtual_file_system.cc:129-158`) walks the in-memory `Entry` tree. It does NOT re-stat the host FS. The tree is populated: * **Eagerly at mount time** by `HostPathDevice::Initialize` → `HostPathDevice::PopulateEntry` (`xenia-canary/src/xenia/vfs/devices/host_path_device.cc:31-75`) which recursively walks the host directory and builds `HostPathEntry` instances via `HostPathEntry::Create`. * **Per-create** by `Entry::CreateEntry` / `HostPathEntry::CreateEntryInternal` (`xenia-canary/src/xenia/vfs/entry.cc:88` / `host_path_entry.cc:78-98`), called from `VirtualFileSystem::OpenFile` → `CreatePath` (`virtual_file_system.cc:160-187`) on successful NtCreateFile create. ### Struct layout `X_FILE_NETWORK_OPEN_INFORMATION` in `xenia-canary/src/xenia/kernel/info/file.h:117-127`: ```cpp struct X_FILE_NETWORK_OPEN_INFORMATION { // 56 bytes be creation_time; be last_access_time; be last_write_time; be change_time; be allocation_size; be end_of_file; be attributes; be pad; }; ``` All multibyte fields are `be<>`, i.e. byte-swapped on write to guest memory (Xbox 360 is big-endian). ### Attribute / size derivation `HostPathEntry::Create` (`xenia-canary/src/xenia/vfs/devices/host_path_entry.cc:32-54`): * `attributes_ = kFileAttributeDirectory (0x10)` if host is a dir. * `attributes_ = kFileAttributeNormal (0x80)` for regular files. `kFileAttributeReadOnly (0x01)` is OR'd in iff `device->is_read_only()`. `cache:` mounts are read-WRITE (`xenia_main.cc:641-651`), so the bit is NOT set. * `size_ = file_info.total_size`. * `allocation_size_ = round_up(total_size, device->bytes_per_sector())`. The base `Device` ctor defaults `bytes_per_sector_ = 512` (`xenia-canary/src/xenia/vfs/device.h`); host-path devices don't override. ### Timestamp format Windows FILETIME — 100-ns ticks since 1601-01-01 UTC. On the Windows debug build, canary derives these via `COMBINE_TIME(WIN32_FILE_ATTRIBUTE_DATA::ftCreationTime)` (`filesystem_win.cc:226-228`), which is already FILETIME-format. ### Why ours diverged at idx 102404 ours's `nt_query_full_attributes_file` previously consulted `std::fs::metadata` directly. On TRUE cold-vs-cold (both engines' cache wiped), the host file `cache/d4ea4615/e/46ee8ca` doesn't exist → ours returned `STATUS_NO_SUCH_FILE`. So why does canary return SUCCESS? The original Phase C+11.1 protocol wiped `~/.local/share/Xenia/cache` but the Windows-debug canary running under wine actually stores its cache under `xenia-canary/build-cross/bin/Windows/Debug/cache/` (canary's storage root resolves to `GetUserFolder()` when no `portable.txt` is present, but the build-cross binary writes there anyway — verified by `Storage root: Z:\home\fabi\RE - Project Sylpheed\xenia-canary\ build-cross\bin\Windows\Debug` in the canary stdout). So the "cold-vs-cold" baseline at C+11.1 was actually ours-cold-vs-canary-warm: canary's binary-dir cache survived the wipe and the pre-existing `46ee8ca` file (May 12 timestamp) made canary's mount-time scan populate the in-memory tree. A TRUE cold-vs-cold (Phase C+12 protocol, see `re-validation.md`) that ALSO wipes the binary-dir cache shows canary ALSO returns NO_SUCH_FILE for `cache:\d4ea4615\e\46ee8ca` at idx 102404 — and the divergence at 102404 dissolves. But the underlying gap remains: canary maintains an in-memory entry tree that ours did not, and any boot where canary has a populated host cache (Sylpheed's normal warm/persistent operation) would see canary succeed where ours misses. Fixing this is in scope per the user directive. ## Architecture decision Architecture **B** (in-memory entry tree mirroring canary): * Add `cache_entries: HashMap` to `KernelState`. * Eager mount-time scan via `populate_cache_entries_from_host` (mirrors canary's `HostPathDevice::PopulateEntry`). * Per-create registration via `register_cache_entry` (mirrors canary's `Entry::CreateEntry`). * `nt_query_full_attributes_file` consults the tree first; defensive host-FS fallback only fires when the tree missed but the file is on disk (refreshes tree as a side effect). Architecture A alone (just stat host FS) would technically work for the specific 102404 case after the corrected cold-vs-cold protocol, but would diverge from canary on any sequence where the in-memory tree's freshness matters (e.g. an entry created earlier this same boot whose host write hasn't flushed yet — rare but possible if a future game pre-allocates entries before the corresponding write). Architecture B keeps semantic parity with canary regardless of the host-FS flush timing. ## Cold-vs-cold protocol correction The C+11.1 protocol wiped `~/.local/share/Xenia/cache`. The Phase C+12 protocol additionally wipes `xenia-canary/build-cross/bin/Windows/Debug/cache/` (plus `cache0/`, `cache1/`) because that's where the Windows-debug canary actually writes its cache under wine. Backup is preserved at `/tmp/canary-binary-cache-backup.tar.gz` (4.7 MB, 23 files) and a copy lives in this audit-run dir as `canary-binary-cache-pre-wipe.tar.gz`. ## File:line citations | canary source | line(s) | what | |---|---|---| | `xboxkrnl_io.cc` | 474-513 | `NtQueryFullAttributesFile_entry` body | | `xboxkrnl_io.cc` | 504 | `change_time = write_timestamp()` | | `xboxkrnl_io.cc` | 512 | `return X_STATUS_NO_SUCH_FILE` | | `info/file.h` | 117-127 | `X_FILE_NETWORK_OPEN_INFORMATION` layout | | `vfs/entry.h` | 67-95 | `kFileAttribute*` consts + accessors | | `vfs/entry.cc` | 88-104 | `Entry::CreateEntry` | | `vfs/virtual_file_system.cc` | 129-158 | `ResolvePath` | | `vfs/devices/host_path_device.cc` | 31-75 | `Initialize` + `PopulateEntry` | | `vfs/devices/host_path_entry.cc` | 32-54 | `HostPathEntry::Create` (attrs + size) | | `base/filesystem_win.cc` | 208-230 | `GetInfo` (FILETIME timestamps) | | `app/xenia_main.cc` | 641-651 | `cache:` mount registration |