Files
xenia-rs/audit-runs/phase-c9-XamContentCreateEnumerator/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

5.5 KiB

Phase C+9 — XamContentCreateEnumerator investigation

Phase A oracle (predecessor C+8 baseline)

canary tid=6 → ours tid=1 main chain, tid_event_idx = 102197:

canary: kernel.return XamContentCreateEnumerator return_value=1317  status=0x00000525  side_effects=[]
ours:   kernel.return XamContentCreateEnumerator return_value=0     status=0x00000000  side_effects=[]

args and args_resolved are empty per the current Phase A emitter scope (kernel.call args are not yet wired for dword-only signatures).

Framing verification (reading-error #28 — canary source supersedes

NT-doc semantics or value-name guesses)

The plan's framing was wrong on the constant identity: 0x525 = 1317 is X_ERROR_NO_SUCH_USER, NOT X_ERROR_NO_CONTENT (xbox.h:113):

#define X_ERROR_NO_SUCH_USER X_RESULT_FROM_WIN32(0x00000525L)

X_ERROR_NO_CONTENT is not even defined in canary's xbox.h. Always verify constants against canary source, never the plan's inline annotations.

Canary body (xenia-canary/src/xenia/kernel/xam/xam_content.cc:129-220)

dword_result_t XamContentCreateEnumerator_entry(
    dword_t user_index, dword_t device_id, dword_t content_type,
    dword_t content_flags, dword_t items_per_enumerate,
    lpdword_t buffer_size_ptr, lpdword_t handle_out) {
  assert_not_null(handle_out);

  auto device_info = device_id == 0 ? nullptr : GetDummyDeviceInfo(device_id);
  if ((device_id && device_info == nullptr) || !handle_out) {
    if (buffer_size_ptr) *buffer_size_ptr = 0;
    return X_E_INVALIDARG;        // 0x80070057
  }

  if (buffer_size_ptr) {
    *buffer_size_ptr = sizeof(XCONTENT_DATA) * items_per_enumerate;
  }                                // sizeof(XCONTENT_DATA) = 0x134

  uint64_t xuid = 0;
  if (user_index != XUserIndexNone /* 0xFE */) {
    const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
    if (!user) return X_ERROR_NO_SUCH_USER;    // 0x525  ← THIS BRANCH
    xuid = user->xuid();
  }

  // ... enumerator init, ListContent over HDD/ODD, fill items ...
  *handle_out = e->handle();
  return X_ERROR_SUCCESS;          // 0
}

Only ONE control-flow path returns 0x525:

  1. user_index != 0xFE (i.e. a specific signed-in user is being queried)
  2. XamState::GetUserProfile(user_index) returns nullptr — i.e. no profile is installed at that slot.

Under canary's default --mute=true config (no --profile_slot_* flags), ProfileManager has zero profiles, so GetProfile(N) returns nullptr for every N.

Side-effects check (rule out classification C)

Canary's kernel.return payload has side_effects=[]. Combined with the body shape: the *handle_out write and the *buffer_size_ptr write happen inside the function but are not handle-creation side effects (no Phase A handle.create emit because no XEnumerator was minted). The empty side_effects confirms canary reached the X_ERROR_NO_SUCH_USER early-return at line 153-155, before the new ContentEnumerator(...) allocation at line 160-161. So:

  • No handle-table mutation to mirror.
  • The *buffer_size_ptr write happens BEFORE the user check (line 145-147), so it does fire on the 0x525 path. Ours must mirror.

Classification

Class A — Unconditional (under ours's state model).

Ours has no profile manager; all user_index < 4 queries return "no such user". This is effectively unconditional from Sylpheed's perspective. Mirror canary's early-return.

NOT class B (state-dependent over real disk content) — we don't reach the ListContent() enumeration path, so we don't need a filesystem scan. No escalation.

NOT class C (side-effect mismatch) — both engines have empty side_effects for this idx (above).

NOT class D (Phase A coverage asymmetry) — both engines emit the same 3-event sequence (import.call, kernel.call, kernel.return).

Ours pre-fix state

crates/xenia-kernel/src/xam.rs:59:

state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success);

stub_success sets r3 = 0. Hence return_value=0, status=0x00000000, no buffer_size_ptr write, no handle_out write.

Fix shape

Replace the stub_success registration with a dedicated handler that:

  1. Reads r3..r9 (user_index, device_id, content_type, content_flags, items_per_enumerate, buffer_size_ptr, handle_out).
  2. Returns X_E_INVALIDARG (0x80070057) when device_id is unknown (>2; canary's GetDummyDeviceInfo accepts HDD=1, ODD=2) or handle_out is NULL; writes *buffer_size_ptr = 0 in that arm.
  3. Otherwise writes *buffer_size_ptr = 0x134 * items_per_enumerate (the sizeof(XCONTENT_DATA) computation).
  4. If user_index != 0xFE: return X_ERROR_NO_SUCH_USER (0x525) — ours has no profile manager.
  5. Otherwise (user_index == 0xFE, XUserIndexNone): proceed to a stubbed enumerator-success path — write *handle_out = 0 and return 0. Defensive; not exercised by current Phase A.

LOC: ~85 body + ~125 tests = ~210 additive in xam.rs only.

Internal-consistency caveat (deferred)

Ours's xam_user_get_signin_state (xam.rs:380-384) returns 1 for user_index == 0, claiming user 0 is signed in. This contradicts the "no profile manager" model used by the C+9 fix. The conflict surfaces at Phase A idx 107996 (canary returns 0 for XamUserGetSigninState) and will be a separate fix in a future session. Single-fix discipline: do not widen scope here.

Internal-consistency caveat #2 (cosmetic)

The doc comment on xam_user_get_signin_state says "user0 signed in locally" but canary returns 0 here. That's evidence of an older fix that needs reconciliation. Out of scope for C+9.