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>
This commit is contained in:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -0,0 +1,131 @@
# Phase A diff report
**This report is the output of Phase A's diff harness. Divergences
shown here are INPUT for Phase B (first-divergence localization),
not findings of Phase A.** Phase A's job is to make the harness
itself correct, not to analyze what it surfaces.
## Summary
| canary_tid | ours_tid | matched | canary_total | ours_total | first_divergence_at |
|---|---|---|---|---|---|
| 4 | 11 | 9 | 27668 | 9 | — |
| 6 | 1 | 102404 | 315020 | 108471 | 102404 |
| 7 | 2 | 29 | 29 | 30 | — |
| 12 | 7 | 2 | 3404 | 3 | 2 |
| 14 | 9 | 39 | 667692 | 75 | 39 |
| 15 | 10 | 15 | 417763 | 15 | — |
## canary_tid=4 → ours_tid=11
No divergence within the 9 compared events (canary has 27668, ours has 9).
## canary_tid=6 → ours_tid=1
First divergence at `tid_event_idx=102404`: payload.return_value: canary=0 ours=3221225524
**Pre-context (last 5 matching events):**
```
canary: [102399] import.call RtlInitAnsiString
ours: [102399] import.call RtlInitAnsiString
canary: [102400] kernel.call RtlInitAnsiString
ours: [102400] kernel.call RtlInitAnsiString
canary: [102401] kernel.return RtlInitAnsiString
ours: [102401] kernel.return RtlInitAnsiString
canary: [102402] import.call NtQueryFullAttributesFile
ours: [102402] import.call NtQueryFullAttributesFile
canary: [102403] kernel.call NtQueryFullAttributesFile
ours: [102403] kernel.call NtQueryFullAttributesFile
```
**Divergent event:**
```
canary: [102404] kernel.return NtQueryFullAttributesFile
ours: [102404] kernel.return NtQueryFullAttributesFile
```
**Next event after the divergence (if any):**
```
canary: [102405] import.call RtlEnterCriticalSection
ours: [102405] import.call RtlNtStatusToDosError
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 773917700, "kind": "kernel.return", "payload": {"name": "NtQueryFullAttributesFile", "return_value": 0, "side_effects": [], "status": "0x00000000"}, "schema_version": 1, "tid": 6, "tid_event_idx": 102404}
{"deterministic": true, "engine": "ours", "guest_cycle": 5391947, "host_ns": 470401201, "kind": "kernel.return", "payload": {"name": "NtQueryFullAttributesFile", "return_value": 3221225524, "side_effects": [], "status": "0xc0000034"}, "schema_version": 1, "tid": 1, "tid_event_idx": 102404}
```
## canary_tid=7 → ours_tid=2
No divergence within the 29 compared events (canary has 29, ours has 30).
## canary_tid=12 → ours_tid=7
First divergence at `tid_event_idx=2`: payload.return_value: canary=258 ours=0
**Pre-context (last 5 matching events):**
```
canary: [0] import.call KeWaitForSingleObject
ours: [0] import.call KeWaitForSingleObject
canary: [1] kernel.call KeWaitForSingleObject
ours: [1] kernel.call KeWaitForSingleObject
```
**Divergent event:**
```
canary: [2] kernel.return KeWaitForSingleObject
ours: [2] kernel.return KeWaitForSingleObject
```
**Next event after the divergence (if any):**
```
canary: [3] import.call RtlEnterCriticalSection
ours: <end of stream>
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 922327400, "kind": "kernel.return", "payload": {"name": "KeWaitForSingleObject", "return_value": 258, "side_effects": [], "status": "0x00000102"}, "schema_version": 1, "tid": 12, "tid_event_idx": 2}
{"deterministic": true, "engine": "ours", "guest_cycle": 30, "host_ns": 495749641, "kind": "kernel.return", "payload": {"name": "KeWaitForSingleObject", "return_value": 0, "side_effects": [], "status": "0x00000000"}, "schema_version": 1, "tid": 7, "tid_event_idx": 2}
```
## canary_tid=14 → ours_tid=9
First divergence at `tid_event_idx=39`: payload.ord: canary=503 ours=293
**Pre-context (last 5 matching events):**
```
canary: [34] kernel.call KeReleaseSpinLockFromRaisedIrql
ours: [34] kernel.call KeReleaseSpinLockFromRaisedIrql
canary: [35] kernel.return KeReleaseSpinLockFromRaisedIrql
ours: [35] kernel.return KeReleaseSpinLockFromRaisedIrql
canary: [36] import.call KfLowerIrql
ours: [36] import.call KfLowerIrql
canary: [37] kernel.call KfLowerIrql
ours: [37] kernel.call KfLowerIrql
canary: [38] kernel.return KfLowerIrql
ours: [38] kernel.return KfLowerIrql
```
**Divergent event:**
```
canary: [39] import.call XAudioGetVoiceCategoryVolumeChangeMask
ours: [39] import.call RtlEnterCriticalSection
```
**Next event after the divergence (if any):**
```
canary: [40] kernel.call XAudioGetVoiceCategoryVolumeChangeMask
ours: [40] kernel.call RtlEnterCriticalSection
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 1125054700, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "XAudioGetVoiceCategoryVolumeChangeMask", "ord": 503}, "schema_version": 1, "tid": 14, "tid_event_idx": 39}
{"deterministic": true, "engine": "ours", "guest_cycle": 417, "host_ns": 1686986655, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "RtlEnterCriticalSection", "ord": 293}, "schema_version": 1, "tid": 9, "tid_event_idx": 39}
```
## canary_tid=15 → ours_tid=10
No divergence within the 15 compared events (canary has 417763, ours has 15).

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000002,
"imports": 40465,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000002,
"imports": 40465,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000002,
"imports": 40465,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,212 @@
# Phase C+10 — NtQueryFullAttributesFile — ESCALATION
## Outcome
**Phase 1 (emitter extension) — LANDED**.
**Phase 4 fix (cache-state seeding) — ESCALATED**, deferred to a
dedicated cache-subsystem session.
The Phase A emitter now resolves OBJECT_ATTRIBUTES path arguments on
both engines (cvar-gated, default-off, behaviorally inert when off).
That permanent infrastructure win surfaces the divergence string for
this and every future file-IO divergence.
The actual cache-seeding fix needed to advance main matched-prefix
past 102,404 is out of scope per the user's escalation criteria.
## Captured framing (post-extension)
Both engines now log the resolved path at `kernel.call.args_resolved`:
```
canary[6][102403]: NtQueryFullAttributesFile args_resolved.path = "cache:\\d4ea4615\\e\\46ee8ca"
ours [1][102403]: NtQueryFullAttributesFile args_resolved.path = "cache:\\d4ea4615\\e\\46ee8ca"
canary[6][102404]: kernel.return return_value = 0 (STATUS_SUCCESS)
ours [1][102404]: kernel.return return_value = 0xC0000034 (STATUS_OBJECT_NAME_NOT_FOUND)
```
Both engines query the **same path**. Canary returns SUCCESS because
its cache directory (`/home/fabi/.local/share/Xenia/cache/`) is
**pre-populated** with 23 files (~5 MB) accumulated over prior
Sylpheed boots. Ours's cache directory is fresh-wiped per AUDIT-038.
After this query, canary follows up with `NtCreateFile` for the same
path (idx 102481) — it actually reads the cached data. So just lying
SUCCESS without backing bytes would only push the divergence ~78
events forward.
## Classification (per plan Phase 4)
**(A) Missing file — narrowly true (this single cache entry), but**
**(D) Subsystem-required — actual scope**.
Choices considered:
1. **Plant a single file**: would only push the divergence to the
next cache-existence query (16+ distinct hashes in
`cache:\<HASH1>\<X>\<HASH2>` form). 23 files in canary's cache,
most of them follow this pattern. After each plant the next
query still misses.
2. **Seed ours's cache from canary's**: 23 files, ~5 MB. Mechanically
easy (~30 LOC `copy_dir_all`) but violates AUDIT-038's no-oracle-
state line AND AUDIT-053's documented warm-start regression
(Sylpheed's `cache:\*.tmp` journal-style writes append per boot,
making a naive persistent seed self-inconsistent after the second
boot — `runtime_error` throws from version-check on reload).
3. **Lie SUCCESS on cache: existence + lie SUCCESS on subsequent
NtCreateFile + return zero-byte file**: changes Nt semantics
game-wide, likely breaks any read that expects valid content.
4. **Implement the game's cache-generation logic**: that's the
shader/PSO/material cache build subsystem — multi-hundred-LOC
generative subsystem, not in scope.
The user's escalation criteria explicitly call out
"cache-population infrastructure" as ESCALATION. Choices 2-4 fit
that. Choice 1 doesn't solve the problem.
## What was landed (Phase 1 only)
Permanent emitter extension on both engines, schema-v1-compatible
(`args_resolved` was already part of v1, this just populates it for
OBJECT_ATTRIBUTES*-taking exports).
### Ours side (~50 LOC additive)
- `xenia-rs/crates/xenia-kernel/src/event_log.rs`:
- New `emit_kernel_call_with_path(tid, cycle, name, Option<&str>)`
that mirrors `emit_kernel_call` but adds
`args_resolved:{"path":"..."}` when the path is non-empty.
Degrades to the existing empty-object form otherwise so output
is byte-identical to pre-extension when the path is null.
- `xenia-rs/crates/xenia-kernel/src/path.rs`:
- New `object_attributes_raw_name(mem, ptr) -> Option<String>`
that returns the **raw** trimmed path (no prefix-strip, no
case-fold). The emitter uses raw form so the diff surfaces
upstream differences (e.g. if one engine called with one prefix
and the other with a different prefix), not just post-normalize
differences.
- `xenia-rs/crates/xenia-kernel/src/state.rs`:
- In `call_export`, when `phase_a_on` and `name` matches one of
`{NtCreateFile, NtOpenFile, NtQueryFullAttributesFile,
NtOpenSymbolicLinkObject}`, resolve OBJECT_ATTRIBUTES* from the
appropriate gpr position (verified against canary's
xboxkrnl_io.cc signatures) and call
`emit_kernel_call_with_path`. Otherwise call the legacy
`emit_kernel_call`.
### Canary side (~80 LOC additive)
- `xenia-canary/src/xenia/kernel/event_log.h`:
- New `EmitKernelCallWithPath(name, path)` mirroring ours.
- `xenia-canary/src/xenia/kernel/event_log.cc`:
- Implementation of `EmitKernelCallWithPath`.
- New `phase_a_bridge::EmitImportAndCallWithCtx(module, ord, name,
ppc_context)` that dispatches by `name` to read OBJECT_ATTRIBUTES
from the PPCContext gpr and call the path-bearing form. Falls
back to the legacy form when name doesn't match.
- Helper `ReadObjectAttributesRawName(obj_attrs_ptr)` that mirrors
ours's `object_attributes_raw_name` semantically (raw trimmed,
no normalization).
- `xenia-canary/src/xenia/kernel/util/shim_utils.h`:
- Both trampolines (X::Trampoline / Y::Trampoline) switched from
`EmitImportAndCall(...)` to `EmitImportAndCallWithCtx(...,
ppc_context)`. PPCContext is already in scope at that call site
(it's the first argument the trampoline receives).
Total: ~80 LOC each side. Both behaviorally inert when cvar OFF.
## Gates (Phase 1 extension only — all pass)
| # | gate | result |
|---|---|---|
| 1 | cvar-OFF determinism 50M (3 runs) | PASS — all 3 = `b8fa0e0460359a4f660adb7605e053de` (matches C+9 baseline, unchanged) |
| 2 | Phase B `image_loaded_sha256` | PASS — `ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18` (matches baseline) |
| 3 | Phase A main matched-prefix | UNCHANGED — 102404 (extension was framing-only; no fix landed; no advance expected) |
| 4 | Both engines build clean | PASS |
| 5 | Phase A emitter det fields (2 runs) | PASS — both = `7489e90ef4c9be629af8c9fabb1cbdd7` (new; replaces C+9's `0b299c37…` because the new args_resolved.path field is part of the det signature) |
| 6 | Unit tests | PASS — 165 → 165 (no new, no regressions) |
## Schema status
The args_resolved field is part of schema-v1 already; this Phase only
**populates** it for a subset of exports. No schema version bump.
The schema-v1 example (`schema-v1.md:112`) shows exactly the form we
emit. We are now compliant with the documented schema for path-bearing
exports rather than emitting an empty stub.
## Cascade prediction (resolution / next steps)
| stage | predicted | outcome |
|---|---|---|
| A=extend emitter cleanly | ~80% | LANDED |
| B=capture path string both engines | ~85% | LANDED — `cache:\d4ea4615\e\46ee8ca` matched both engines |
| C=classify root cause | ~75% | DONE — Class D (subsystem-required) |
| D=land fix in scope | ~55% | **ESCALATED** — fix is choice 2-4 above |
| E=main chain advances past 102404 | ~50% | NOT THIS SESSION |
## Reading-error class
NO new class. Existing classes #15 / ζ (VFS layout aliasing,
AUDIT-053) and AUDIT-038 (no oracle state) are re-affirmed:
* Class #15 ζ (AUDIT-053): persistent cache + journal `.tmp` writes
create a warm-start regression.
* AUDIT-038 line: oracle state is forbidden in default boot.
Both rules together make the cache-seeding fix subsystem-tier, not
single-fix-tier.
## Handoff to dedicated cache-subsystem session
The next session targeting this divergence should:
1. **Decide cache-state strategy**:
- (a) Implement Sylpheed's cache-generation logic so ours builds
its own cache from scratch (matches canary's own bootstrap
experience — but multi-hundred-LOC).
- (b) Seed-once-then-persist: copy canary's cache into ours's
cache_root behind a new cvar `--cache-seed-from=<path>`, then
enable persistence. AUDIT-053's warm-start regression must be
re-tested with AUDIT-054's FILE_DIRECTORY_FILE fix in tree
(it landed AFTER 053's regression was observed).
- (c) Hybrid: synthesize a stub success at NtQueryFullAttributesFile
for known-good cache hashes, then synthesize NtCreateFile/Read
responses with bytes captured from canary's cache files. Closest
to a "single missing file plant" but for 23 files.
2. **Re-validate after the fix** that the warm-start regression
identified in AUDIT-053 doesn't recur (AUDIT-054 may have fixed
it; needs explicit re-test).
3. **Expect cascading Phase A divergences**: each cache hash the
game looks up in turn — the divergence at 102,404 is only the
FIRST. After cache:\d4ea4615 is resolved, the game queries
cache:\69d8e45c (idx 103810 already visible in ours.jsonl) and
so on through 16+ distinct hashes per AUDIT-052.
## Files in this audit run
| file | content |
|---|---|
| `escalation.md` | this file |
| `investigation.md` | Phase 1-4 walkthrough |
| `re-validation.md` | gate results (Phase 1 extension only) |
| `ours.jsonl`, `ours-determ.jsonl`, `canary.jsonl` | Phase A logs with new args_resolved field |
| `diff-report.md` | re-run with path field populated |
| `snap/ours/` | Phase B snapshot (unchanged from C+9) |
| `digest-cvaroff-{1,2,3}.json` | 3× determinism (all = C+9 baseline) |
## Next target
**Same idx 102,404 NtQueryFullAttributesFile**, but in a dedicated
cache-subsystem session. Path framing is now captured for the next
investigator's first read.

View File

@@ -0,0 +1,236 @@
# Phase C+10 — NtQueryFullAttributesFile — Investigation
## Phase 1: Emitter extension (LANDED)
### Problem
C+9 left the divergence with no resolved path string:
```
canary[6][102404] kernel.return NtQueryFullAttributesFile return_value=0
ours [1][102404] kernel.return NtQueryFullAttributesFile return_value=0xC0000034
```
`payload.args` and `payload.args_resolved` were both empty objects.
We had no way to identify WHICH file the engine was querying.
### Shape of the fix
Schema v1 already declares `args_resolved` as a free-form object
attached to `kernel.call` (schema-v1.md:108-117), and the existing
example explicitly shows `{"path":"..."}`. The emitter just wasn't
populating it. Extension is pure schema-v1 compliance, no version
bump.
#### Ours-side (event_log.rs / path.rs / state.rs)
- Added `event_log::emit_kernel_call_with_path(tid, cycle, name,
Option<&str>)` — same byte format as `emit_kernel_call`, but when
`path` is `Some(non_empty)` emits `args_resolved:{"path":"..."}`.
When `None` or empty, degrades to the existing
`args_resolved:{}` form so unrelated exports' output is
byte-identical to pre-extension.
- Added `path::object_attributes_raw_name(mem, ptr) -> Option<String>`
— returns the RAW path string (trimmed of whitespace, NO
prefix-strip / no case-fold) so the diff surfaces upstream
prefix-form differences instead of masking them via normalization.
Pre-existing `object_attributes_to_vfs_path` (which DOES normalize)
is kept as-is for VFS lookup callers; emitter uses the new raw
helper.
- `state.rs::call_export`, inside the `phase_a_on` guarded block:
new `match name` resolves OBJECT_ATTRIBUTES* from the right gpr
position. Argument positions verified against canary's
`xboxkrnl/xboxkrnl_io.cc` signatures:
- `NtQueryFullAttributesFile` → r3 = obj_attrs
- `NtOpenSymbolicLinkObject` → r4 = obj_attrs
- `NtCreateFile`, `NtOpenFile` → r5 = obj_attrs
Then calls `emit_kernel_call_with_path(..., resolved.as_deref())`
instead of `emit_kernel_call(...)`. All other exports fall through
to `None` and the legacy form.
#### Canary-side (event_log.h / event_log.cc / util/shim_utils.h)
- `event_log.h`: declared `EmitKernelCallWithPath(name, path)`.
- `event_log.cc`: implemented same as ours (degrades to legacy form
for empty path).
- `event_log.cc::phase_a_bridge::EmitImportAndCallWithCtx(module,
ord, name, ppc_context)` — new bridge function. PPCContext is
passed as `void*` to keep the header transitive include footprint
small (the bridge cc reinterprets to PPCContext* internally).
Inside the bridge, helper `ReadObjectAttributesRawName(ptr)` reads
the X_OBJECT_ATTRIBUTES.name_ptr, then the X_ANSI_STRING bytes
directly out of guest memory (no util::TranslateAnsiPath
normalization). Trims whitespace + trailing NULs to match ours's
semantics byte-for-byte.
- `util/shim_utils.h`: both export trampolines (X::Trampoline /
Y::Trampoline) switched the `phase_a_bridge::EmitImportAndCall`
call to `phase_a_bridge::EmitImportAndCallWithCtx`, passing the
existing `ppc_context` argument that's already in scope. The
legacy `EmitImportAndCall` stays declared and defined for any
future callers that don't have a PPCContext.
### Verification
- Build both engines clean.
- Determinism 3x: digest md5 = `b8fa0e0460359a4f660adb7605e053de`
(identical to C+9 baseline — extension is cvar-OFF zero-cost).
- Phase A emitter determinism 2x: det-fields md5 = `7489e90e…` byte
identical. (Different from C+9's `0b299c37…` because the path
field IS in the deterministic signature — but stable across runs.)
## Phase 2: Re-run + capture path string
After the extension, both engines emit the path at
`kernel.call.args_resolved.path`:
```
canary[6][102403] NtQueryFullAttributesFile path = "cache:\d4ea4615\e\46ee8ca"
ours [1][102403] NtQueryFullAttributesFile path = "cache:\d4ea4615\e\46ee8ca"
```
Both engines query the **same path**. No upstream divergence — the
ANSI_STRING content matches byte-for-byte.
## Phase 3: Why does ours say NOT_FOUND?
### Trace through ours's `nt_query_full_attributes_file`
`exports.rs:1913-1990`:
1. Read OBJECT_ATTRIBUTES → path =
`"cache:/d4ea4615/e/46ee8ca"` (after `normalize_path`).
2. `state.resolve_cache_path(&path)` returns
`Some(<temp_dir>/xenia-rs-cache-<pid>-0/d4ea4615/e/46ee8ca)`.
3. `std::fs::metadata(host_path)` returns `Err(NotFound)`.
4. Return `STATUS_OBJECT_NAME_NOT_FOUND` (`0xC0000034`).
The host path doesn't exist because ours's `init_cache_root`
(`state.rs:499-510`) **clears** the cache directory on every boot
(AUDIT-038 line: per-process tmpdir + full wipe so two consecutive
runs see byte-identical initial state).
### Why does canary's NOT fail?
`xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:474-513`:
1. Read OBJECT_ATTRIBUTES → target_path via TranslateAnsiPath.
2. `kernel_state()->file_system()->ResolvePath(target_path)`.
3. If `entry` found, populate file_info, return `X_STATUS_SUCCESS`.
4. Else return `X_STATUS_NO_SUCH_FILE` (`0xC0000035`).
Canary returns 0 → entry was found. Canary's cache mount is at
`/home/fabi/.local/share/Xenia/cache/` (a persistent host directory
populated over prior boots).
### Verification of canary's cache state
```
$ ls /home/fabi/.local/share/Xenia/cache/d4ea4615/e/
-rw-rw-r-- 1 fabi fabi 400 May 11 21:01 46ee8ca
```
Single 400-byte file. Total cache: 23 files, ~5 MB across 16
distinct top-level hash directories.
### Sibling-cache observations
ours.jsonl shows the SAME `NtQueryFullAttributesFile` fires for
multiple cache paths within the 50M window — all returning
`0xC0000034`. Example: idx 103810 queries
`cache:\69d8e45c\8\3421153`. So the divergence is not a single
missing file but a class of 16+ missing hashes.
## Phase 4: Classification + scope decision
Per the plan, the classes are:
* **(A) Missing file** — a single plant fixes it (small).
* **(B) Path-normalization bug** — string operation (small).
* **(C) VFS mount missing** — add the mount (small-medium).
* **(D) Subsystem-required** — STFS or similar — **ESCALATE**.
* **(E) Upstream divergence** — walk back.
This is **NOT (B)** — both engines normalize identically (verified
by matching args_resolved.path).
This is **NOT (E)** — upstream is bit-identical for 102,403 events.
This is **NOT (A)** for any single file — the game queries 16+
distinct cache hashes; planting one only postpones the divergence.
This is **closest to a hybrid (C+D)**:
* **(C)-ish**: canary's cache MOUNT resolves to a populated host dir;
ours's mount resolves to a wiped tmp dir.
* **(D)-ish**: canary's cache is populated because it ran the game
before and the game **built** the cache. To match canary's state
on a fresh boot, we either:
- implement the game's cache-build logic (subsystem),
- copy canary's pre-built cache (oracle state — AUDIT-038
violation),
- or accept that ours runs cold and the divergence is a
fundamental cold-vs-warm asymmetry.
### AUDIT-053 cross-check (warm-start regression risk)
Per AUDIT-053 memo:
> Phase 2 permanent fix REVERTED — warm-start regression from VFS
> layout aliasing: `open_cache_file` treats all `NtCreateFile` as
> files, but `cache:\d4ea4615 disp=CREATE` is meant as a DIRECTORY.
AUDIT-054 fixed that specific aliasing (FILE_DIRECTORY_FILE bit
threading). But there's still the AUDIT-053 secondary concern:
Sylpheed's `cache:\<hash>.tmp` journal-style writes append on each
boot — making naive persistence self-inconsistent across boots.
Whether AUDIT-054's fix fully unblocks persistence is **NOT
RE-VERIFIED** in this session. Re-testing the AUDIT-053 regression
under AUDIT-054's fix-in-tree is itself a follow-up.
### Scope per user direction
User said:
> If the fix requires major VFS work, STFS subsystem
> implementation, or cache-population infrastructure: ESCALATE.
Choices 2-4 from `escalation.md` all qualify as "cache-population
infrastructure":
* Choice 1 (single file plant) won't solve the problem (16+ hashes).
* Choice 2 (seed from canary) is oracle state + warm-start regression
risk per AUDIT-053.
* Choice 3 (synthesize cache reads) is multi-export semantic-change.
* Choice 4 (build cache from scratch) is a full subsystem.
**ESCALATION declared.** Phase 1 emitter extension landed as the
session's permanent infrastructure contribution.
## Discipline check
* **Reading-error #28** (canary source-of-truth): verified canary's
actual `NtQueryFullAttributesFile_entry` body
(`xboxkrnl_io.cc:474-513`), did not assume.
* **Reading-error #23** (downstream regression): no fix landed, so
no regression risk. Emitter extension is cvar-OFF zero-cost.
* **Escalation discipline**: triggered cleanly; explicit memo;
contributing infrastructure (emitter path resolution) kept.
* **Path encoding**: ANSI_STRING raw bytes captured; both engines
agree byte-for-byte; no Unicode issues for the queried path.
* **AUDIT-054 deferred-item**: not re-touched. Cache persistence
remains opt-in via `XENIA_CACHE_PERSIST=1`. Default keeps the
AUDIT-038 wipe behavior.
* **`--mute=true`**: every canary run.
* **Renamed binaries**: `xrs-c10` / `xc-c10.exe`.
## Confidence
* **Phase 1 emitter extension**: HIGH — schema-compliant, additive,
cvar-OFF zero-cost verified via determinism.
* **Phase 4 classification**: HIGH — three independent observations
agree (canary cache populated, ours cache wiped, multiple hashes).
* **Cascade prediction at 102,404**: cache fix lands only the
FIRST in a series — next cache hash will be the next divergence.
Likely net delta of several hundred to a few thousand matched
events per cache slot resolved, until a non-cache divergence
appears.

View File

@@ -0,0 +1,106 @@
# Phase C+10 — NtQueryFullAttributesFile — Re-validation
Validation against the Phase 1 emitter extension (only landed work).
No fix landed for the actual divergence (ESCALATED — see
`escalation.md`).
## Gate matrix
| # | gate | result |
|---|---|---|
| 1 | cvar-OFF determinism 50M (3 runs) | PASS — all 3 = `b8fa0e0460359a4f660adb7605e053de` (UNCHANGED from C+9 baseline; extension is zero-cost when cvar OFF) |
| 2 | Phase B `image_loaded_sha256` | PASS — `ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18` (matches baseline; XEX loader untouched) |
| 3 | Phase A main matched-prefix | UNCHANGED — 102404 (no fix landed; matched-prefix advance NOT expected this session) |
| 3b | tid=4 → 11 unchanged | PASS — 9 (no regression) |
| 3c | tid=7 → 2 unchanged | PASS — 29 (no regression) |
| 3d | tid=12 → 7 unchanged | PASS — 2 (no regression) |
| 3e | tid=14 → 9 unchanged | PASS — 39 (no regression) |
| 3f | tid=15 → 10 unchanged | PASS — 15 (no regression) |
| 4 | Both engines build clean | PASS (1 unrelated `walk_committed_regions` dead-code warning, pre-existing C+9) |
| 5 | Phase A emitter determinism (2 runs) | PASS — both = `7489e90ef4c9be629af8c9fabb1cbdd7` |
| 6 | Unit tests | PASS — 165 → 165 (no new tests; no regressions) |
## Stable-digest comparison
| field | C+9 baseline | C+10 post-extension | delta |
|---|---|---|---|
| (all stable fields, 3 runs) | `b8fa0e0460359a4f660adb7605e053de` | `b8fa0e0460359a4f660adb7605e053de` | 0 |
Extension is purely emitter-side, cvar-gated default-off, behaviorally
inert. Determinism unchanged.
## Phase A determinism
```
ours.jsonl (run 1) det-fields md5: 7489e90ef4c9be629af8c9fabb1cbdd7
ours-determ.jsonl (run 2) det-fields md5: 7489e90ef4c9be629af8c9fabb1cbdd7
```
Byte-identical on deterministic fields. New `--phase-a` det baseline
`7489e90e…` (replaces C+9's `0b299c37…`). The signature changed
because the new `args_resolved.path` field IS part of the
deterministic payload — but it's stable across runs (path string is
read directly out of guest memory, fully deterministic for fixed
input).
## Per-chain summary
| chain | C+9 baseline | C+10 (after emitter extension) | delta |
|---|---|---|---|
| canary tid=6 → ours tid=1 (main) | 102404 | 102404 | 0 (no fix landed) |
| canary tid=4 → ours tid=11 | 9 | 9 | 0 |
| canary tid=7 → ours tid=2 | 29 | 29 | 0 |
| canary tid=12 → ours tid=7 | 2 | 2 | 0 |
| canary tid=14 → ours tid=9 | 39 | 39 | 0 |
| canary tid=15 → ours tid=10 | 15 | 15 | 0 |
All chains unchanged. No regressions. No advances (as expected — no
behavioral change to either engine).
## Confirmed framing (Phase 1 success)
The divergence path is now visible in the diff output:
```
canary[6][102403] kernel.call NtQueryFullAttributesFile
args_resolved.path = "cache:\d4ea4615\e\46ee8ca"
ours [1][102403] kernel.call NtQueryFullAttributesFile
args_resolved.path = "cache:\d4ea4615\e\46ee8ca"
```
Both engines query the SAME path. Both successfully read the
OBJECT_ATTRIBUTES.name_ptr. Divergence is purely in
`vfs.stat`/`file_system().ResolvePath` outcome (canary's mount has
the file; ours's wiped cache doesn't).
## Sources of truth for the path field
Both engines read directly out of guest memory at the same point in
the dispatch sequence (right before the export handler runs). The
resulting string is byte-identical when input is identical. This is
verified by the byte-identical det-fields md5 across runs.
## Phase 6 status
Gates 1-2, 3b-3f, 4-6 all pass. Gate 3 main-prefix did NOT advance.
This is because **no fix was landed** — Phase 4 ESCALATED.
The session's contribution is:
* Permanent emitter extension on both engines (~80 LOC each).
* Path framing captured for the divergence.
* Classification + scope-decision memo in `escalation.md`.
## Next target
Same idx 102,404, but in a dedicated cache-subsystem session that
can:
1. Re-test AUDIT-053's warm-start regression under AUDIT-054's
FILE_DIRECTORY_FILE fix.
2. Decide between cache-build subsystem vs cache-seed-from-canary vs
stub-success.
3. Land + re-validate the cache-state mechanism + run Phase A to
measure the cascade.
The path framing landed in this session is the permanent input for
that follow-up session.

View File

@@ -0,0 +1,25 @@
{
"build_id": "ours-phaseB",
"cvars": {
"phase_b_dump_section_content": false,
"phase_b_snapshot_and_exit": false,
"phase_b_snapshot_dir": "audit-runs/phase-c10-NtQueryFullAttributesFile/snap"
},
"deterministic_skip": [
"host_ns_at_snapshot",
"wall_clock_iso8601",
"build_id",
"iso_path",
"cvars.phase_b_snapshot_dir"
],
"engine": "ours",
"host_ns_at_snapshot": 0,
"image_loaded_sha256": "ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18",
"iso_path": "",
"schema_version": 1,
"wall_clock_iso8601": "epoch:0",
"xex_entry_point": "0x824ab748",
"xex_header_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"xex_image_base": "0x82000000",
"xex_image_size": 9568256
}

View File

@@ -0,0 +1,234 @@
{
"cr": [
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0"
],
"ctr": "0x0000000000000000",
"deterministic_skip": [
"hw_id"
],
"engine": "ours",
"fpr": [
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"fpscr": "0x00000000",
"gpr": [
"0x0000000000000000",
"0x00000000700fff00",
"0x0000000020000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x000000007fff0000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"hw_id": 0,
"lr": "0x00000000bcbcbcbc",
"msr": "0x0000000000009030",
"pc": "0x824ab748",
"pcr_base": "0x7fff0000",
"schema_version": 1,
"stack_base": "0x00000000",
"stack_limit": "0x00000000",
"thread_id": 1,
"tls_base": "0x00000000",
"vr": [
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000"
],
"vrsave": "0xffffffff",
"vscr": "00000000000000000000000000010000",
"xer": {
"ca": 0,
"ov": 0,
"so": 0,
"tbc": 0
}
}

View File

@@ -0,0 +1,62 @@
{
"deterministic_skip": [
"raw_handle_id",
"exports_registered_count"
],
"engine": "ours",
"exports_registered_count": 199,
"exports_registered_sample": [
"xam.xex!NetDll_WSACleanup",
"xam.xex!NetDll_WSAStartup",
"xam.xex!XGetAVPack",
"xam.xex!XGetGameRegion",
"xam.xex!XGetLanguage",
"xam.xex!XGetVideoMode",
"xam.xex!XMsgInProcessCall",
"xam.xex!XMsgStartIORequest",
"xam.xex!XMsgStartIORequestEx",
"xam.xex!XNotifyGetNext",
"xam.xex!XNotifyPositionUI",
"xam.xex!XamAlloc",
"xam.xex!XamContentClose",
"xam.xex!XamContentCreate",
"xam.xex!XamContentCreateEnumerator",
"xam.xex!XamContentDelete",
"xam.xex!XamContentGetCreator",
"xam.xex!XamContentGetDeviceData",
"xam.xex!XamContentGetDeviceName",
"xam.xex!XamContentGetDeviceState",
"xam.xex!XamContentSetThumbnail",
"xam.xex!XamEnableInactivityProcessing",
"xam.xex!XamEnumerate",
"xam.xex!XamFree",
"xam.xex!XamGetExecutionId",
"xam.xex!XamGetSystemVersion",
"xam.xex!XamInputGetCapabilities",
"xam.xex!XamInputGetKeystrokeEx",
"xam.xex!XamInputGetState",
"xam.xex!XamInputSetState",
"xam.xex!XamLoaderLaunchTitle",
"xam.xex!XamLoaderTerminateTitle"
],
"exports_registered_sha256": "bca7668a2a76ce1d1cc4dba8a862a2f16ec6ee3b2aab8a71d8d8bc0ccc89a097",
"handle_name_table": [],
"notification_listeners": [],
"objects": [
{
"details": {
"entry_pc": "0x824ab748",
"exit_code": null,
"hw_id": 0,
"is_entry_thread": true,
"thread_id": 1
},
"handle_semantic_id": "9879c5053fedb1d0",
"name": null,
"raw_handle_id": "0x00001000",
"type": "Thread",
"type_code": 5
}
],
"schema_version": 1
}

View File

@@ -0,0 +1,11 @@
{
"engine": "ours",
"files": {
"config.json": "6c758b063ac9da341cb35c7319c68e5ef74543d206c9afa8b7c15d00edd293aa",
"cpu_state.json": "4e6df54ca1939d08854f3a52b49ed2c5ee0823d63cdecad8a7395203dac5443a",
"kernel.json": "2db219d4ca8b0313e53be379b8fcf90ab13b99116e6fac5601f6bdefd1aa6900",
"memory.json": "b96ae4daebfbdd314e574492c1e162f532fa4f89ff5c0d7c6c29743797089cf1",
"vfs.json": "97bb2bda57266d8e0dd1da13309eab5ece43130ef378a0b682917d299e9dc4e1"
},
"schema_version": 1
}

View File

@@ -0,0 +1,84 @@
{
"committed_pages_total": 2594,
"deterministic_skip": [
"host_base_pointer"
],
"engine": "ours",
"guest_address_space_bytes": 4294967296,
"heaps": [
{
"base": "0x00000000",
"name": "v00000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 0
},
"size": "0x40000000"
},
{
"base": "0x40000000",
"name": "v40000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 266
},
"size": "0x40000000"
},
{
"base": "0x80000000",
"name": "v80000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 2336
},
"size": "0x40000000"
},
{
"base": "0x90000000",
"name": "v90000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 0
},
"size": "0x40000000"
}
],
"page_size": 4096,
"regions": [
{
"byte_count": 1048576,
"end": "0x70100000",
"protect": 0,
"section_kind": null,
"sha256": "30e14955ebf1352266dc2ff8067e68104607e750abb9d3b36582b8af909fcb58",
"start": "0x70000000"
},
{
"byte_count": 4096,
"end": "0x7ffe1000",
"protect": 0,
"section_kind": null,
"sha256": "ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7",
"start": "0x7ffe0000"
},
{
"byte_count": 4096,
"end": "0x7fff1000",
"protect": 0,
"section_kind": null,
"sha256": "e35cddaf9c210aed7505ec4cf1c599f58ac2b7ec25b0885db1ee49aba2db519a",
"start": "0x7fff0000"
},
{
"byte_count": 9568256,
"end": "0x82920000",
"protect": 0,
"section_kind": null,
"sha256": "ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18",
"start": "0x82000000"
}
],
"regions_walked": [],
"schema_version": 1,
"section_contents": null
}

View File

@@ -0,0 +1,71 @@
{
"cache_root_listing": [],
"deterministic_skip": [
"host_path_realpath"
],
"engine": "ours",
"mounted_devices_observed_count": 1,
"resolve_path_probes": [
{
"is_directory": true,
"path": "\\Device\\Cdrom0",
"resolved": true,
"size": null
},
{
"is_directory": true,
"path": "\\Device\\Cdrom0\\dat",
"resolved": true,
"size": 4096
},
{
"is_directory": null,
"path": "\\Device\\Cdrom0\\dat\\movie",
"resolved": false,
"size": null
},
{
"is_directory": null,
"path": "\\Device\\Cdrom0\\dat\\movie\\opening.bik",
"resolved": false,
"size": null
},
{
"is_directory": false,
"path": "\\Device\\Cdrom0\\default.xex",
"resolved": true,
"size": 3497984
},
{
"is_directory": null,
"path": "\\Device\\HardDisk0\\Partition1",
"resolved": false,
"size": null
},
{
"is_directory": true,
"path": "cache:\\",
"resolved": true,
"size": null
},
{
"is_directory": null,
"path": "cache:\\nonexistent_probe",
"resolved": false,
"size": null
},
{
"is_directory": true,
"path": "game:\\dat",
"resolved": true,
"size": 4096
},
{
"is_directory": false,
"path": "game:\\default.xex",
"resolved": true,
"size": 3497984
}
],
"schema_version": 1
}