Files
xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/investigation.md
MechaCat02 ef93a4fa14 handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:19:08 +02:00

7.5 KiB

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:

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::InitializeHostPathDevice::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::OpenFileCreatePath (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:

struct X_FILE_NETWORK_OPEN_INFORMATION {  // 56 bytes
  be<uint64_t> creation_time;
  be<uint64_t> last_access_time;
  be<uint64_t> last_write_time;
  be<uint64_t> change_time;
  be<uint64_t> allocation_size;
  be<uint64_t> end_of_file;
  be<uint32_t> attributes;
  be<uint32_t> 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<String, CacheEntryMeta> 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