AUDIT-054: thread CreateOptions through NtCreateFile + opt-in cache persistence

Track A — FILE_DIRECTORY_FILE handling. NtCreateFile's 9th parameter
`create_options` (sp+0x54 per shim_utils.h:49-50) is now read and
forwarded to open_vfs_file/open_cache_file. When the
FILE_DIRECTORY_FILE bit (0x1) is set on a `cache:\<hash>` path,
the host-side handler `mkdir -p`s instead of `File::create`'ing a
0-byte sentinel that blocked subsequent hierarchical creates of
`cache:\<hash>\<sub>\<leaf>` with NAME_COLLISION. Confirmed by
`opts=0x4021` (incl. FILE_DIRECTORY_FILE) on `cache:\d4ea4615`
and `opts=0x4020` (no DIR bit) on the leaf `.tmp` files. NtOpenFile
forwards `open_options` (r8) into the same slot per
xboxkrnl_io.cc:118-122. Closes the AUDIT-053 ζ-class VFS layout
aliasing wedge.

Track B — opt-in persistent cache root. AUDIT-038's per-process
tmpdir + wipe stays the default (preserves lockstep/oracle
determinism + dodges Sylpheed's `<hash>.tmp` journal-append-on-
reboot self-inconsistency). Persistence is now opt-in via
  * `XENIA_CACHE_ROOT=<path>` — explicit path (caller manages
    wiping); hands a stable place to drop a canary-built cache
    for cascade A/B oracle work.
  * `XENIA_CACHE_PERSIST=1` — `$XDG_DATA_HOME/xenia-rs/cache`
    (or `$HOME/.local/share/xenia-rs/cache`).

Cold-start (-n 500M, default tmpfs) with FILE_DIRECTORY_FILE fix:
swaps=1 draws=0 imports=40454 cxx_throw=0 — matches master baseline,
no regression. Cache hierarchy now mkdir-p'd correctly: `cache:/`
contains 9 hash dirs (e.g. `d4ea4615/e/`, `aab216c3/5/`) instead
of the 0-byte sentinel files AUDIT-053 found masquerading as
directories.

LOC: +88 / -14 = +74 net (≤80 budget). All 127 xenia-kernel unit
tests pass.

Trace: audit-runs/audit-054-vfs-layout-fix/
  cold-start-digest.json + warm-start-digest.json (defaults)
  persist-cold-digest.json + persist-warm-digest.json (opt-in)
  baseline-master-digest.json (master 25704c5 reference)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-10 18:11:04 +02:00
parent 25704c5811
commit 2a8ff9515d
2 changed files with 137 additions and 22 deletions

View File

@@ -334,20 +334,23 @@ impl KernelState {
};
crate::exports::register_exports(&mut state);
crate::xam::register_exports(&mut state);
// AUDIT-038set up a deterministic per-process cache root by
// default. Each new `KernelState` lives in its own tmpdir, named
// with the host pid + a monotonic counter so concurrent tests
// don't collide. Errors here are non-fatal (cache I/O degrades
// to the legacy synth-stub fallback) but logged.
static NEXT_CACHE_ID: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let id = NEXT_CACHE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"xenia-rs-cache-{}-{}",
std::process::id(),
id
));
if let Err(e) = state.init_cache_root(root.clone()) {
// AUDIT-054cache root selection. Defaults to AUDIT-038's
// per-process tmpdir + wipe (lockstep determinism + avoids
// the journal-append-on-reboot self-inconsistency Sylpheed's
// `<hash>.tmp` writes produce). Opt-in to persistence via
// `XENIA_CACHE_ROOT=<path>` (explicit) or
// `XENIA_CACHE_PERSIST=1` (`$XDG_DATA_HOME/xenia-rs/cache`).
// Errors are non-fatal (cache I/O degrades to the synth-stub
// fallback) but logged.
let (root, wipe) = resolve_default_cache_root();
let init_result = if wipe {
state.init_cache_root(root.clone())
} else {
std::fs::create_dir_all(&root).map(|()| {
state.cache_root = Some(root.clone());
})
};
if let Err(e) = init_result {
tracing::warn!(
"Failed to initialise cache root at {:?}: {} — cache:/* opens \
will fall back to the synth-empty-file stub",
@@ -397,6 +400,13 @@ impl KernelState {
Ok(())
}
/// AUDIT-054 — direct (non-wiping) cache-root install for tests
/// that want byte-for-byte control over what's already on disk
/// when the kernel boots. Skips the `init_cache_root` clear pass.
pub fn set_cache_root(&mut self, root: std::path::PathBuf) {
self.cache_root = Some(root);
}
/// Resolve a guest VFS path (e.g. `cache:\d4ea4615e46ee8ca.tmp`) to
/// the host-FS path that backs it. Returns `None` if the path doesn't
/// have a `cache:` prefix or if no cache root is mounted (legacy
@@ -1016,6 +1026,63 @@ impl Default for KernelState {
}
}
/// AUDIT-054 — pick the cache root path + wipe-on-init mode for a
/// fresh `KernelState`.
///
/// Default behaviour matches AUDIT-038: per-process tmpdir + full
/// wipe so two consecutive runs see byte-identical initial state
/// (lockstep / oracle determinism). AUDIT-054 found that Sylpheed's
/// `cache:\<hash>.tmp` journal-style writes append on each boot, so
/// a naive persistent root makes the on-disk state self-inconsistent
/// after the second boot (`runtime_error` throws from version-check
/// on reload). Opt-in to persistence via env:
/// * `XENIA_CACHE_ROOT=<path>` — explicit persistent path. Caller
/// is responsible for wiping when needed.
/// * `XENIA_CACHE_PERSIST=1` — use `$XDG_DATA_HOME/xenia-rs/cache`
/// (or `$HOME/.local/share/xenia-rs/cache`) without wiping.
///
/// Returns `(root, wipe)` where `wipe = true` triggers the
/// `init_cache_root` clear-then-recreate dance.
fn resolve_default_cache_root() -> (std::path::PathBuf, bool) {
if let Ok(p) = std::env::var("XENIA_CACHE_ROOT") {
if !p.is_empty() {
return (std::path::PathBuf::from(p), false);
}
}
let persist = std::env::var("XENIA_CACHE_PERSIST")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if persist {
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return (
std::path::PathBuf::from(xdg).join("xenia-rs/cache"),
false,
);
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return (
std::path::PathBuf::from(home).join(".local/share/xenia-rs/cache"),
false,
);
}
}
}
static NEXT_CACHE_ID: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let id = NEXT_CACHE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
(
std::env::temp_dir().join(format!(
"xenia-rs-cache-{}-{}",
std::process::id(),
id
)),
true,
)
}
/// KRNBUG-AUDIT-003. Outcome of probing a guest pointer as the `this`
/// of a C++ object: read `[this]` as the vtable, then attempt MSVC
/// RTTI to recover the decorated class name. Pure read; lockstep-safe.