Files
xenia-rs/audit-runs/phase-c7-XamTaskCloseHandle/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

4.9 KiB

Phase C+7 — XamTaskCloseHandle (idx=102158)

Step 1 — Framing verification (reading-error #28 discipline)

Canary source (authoritative)

xenia-canary/src/xenia/kernel/xam/xam_task.cc:83-93:

dword_result_t XamTaskCloseHandle_entry(dword_t obj_handle) {
  const X_STATUS error_code = xboxkrnl::NtClose(obj_handle);

  if (XFAILED(error_code)) {
    const uint32_t result = xboxkrnl::xeRtlNtStatusToDosError(error_code);
    XThread::SetLastError(result);
    return false;
  }

  return true;
}
DECLARE_XAM_EXPORT1(XamTaskCloseHandle, kNone, kImplemented);

xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_ob.cc:414-416:

uint32_t NtClose(uint32_t handle) {
  return kernel_state()->object_table()->ReleaseHandle(handle);
}

xenia-canary/src/xenia/kernel/util/object_table.cc:189-208:

X_STATUS ObjectTable::ReleaseHandle(X_HANDLE handle) { /* lock */ return ReleaseHandleInLock(handle); }
X_STATUS ObjectTable::ReleaseHandleInLock(X_HANDLE handle) {
  ObjectTableEntry* entry = LookupTableInLock(handle);
  if (!entry) {
    return X_STATUS_INVALID_HANDLE;
  }
  if (--entry->handle_ref_count == 0) {
    return RemoveHandle(handle);
  }
  return X_STATUS_SUCCESS;
}

Framing CONFIRMED. The C+6½ XAM audit was accurate this time:

  • XamTaskCloseHandle_entry body calls xboxkrnl::NtClose(obj_handle).
  • Returns true (=1) on XSUCCESS, false (=0) on XFAILED (after setting last error).
  • Signature: dword_result_t — emitter records ctx.gpr[3] as the dword return.

Reading-error #28 lesson holds — and this time, the framing was right. The check itself is the discipline.

Ours source (pre-fix)

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

state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);

stub_success (line 103) sets ctx.gpr[3] = 0. That's the divergence.

Phase A event context (idx 102156-102159 main chain)

canary tid=6 [102156] import.call XamTaskCloseHandle (ord 433)
canary tid=6 [102157] kernel.call XamTaskCloseHandle args={} args_resolved={}
canary tid=6 [102158] kernel.return XamTaskCloseHandle return_value=1
canary tid=6 [102159] import.call KeWaitForSingleObject

ours   tid=1 [102156] import.call XamTaskCloseHandle (ord 433)
ours   tid=1 [102157] kernel.call XamTaskCloseHandle args={} args_resolved={}
ours   tid=1 [102158] kernel.return XamTaskCloseHandle return_value=0  ← DIVERGENT
ours   tid=1 [102159] import.call KeWaitForSingleObject

args={} in both engines — the kernel.call args record is empty for XAM, so the handle being closed is not directly visible in the JSONL. Per canary's XamTaskSchedule_entry, the handle was returned via the out-pointer from the preceding XamTaskSchedule at idx 102154 (the 12345 placeholder per xam_task.cc:48).

Step 3 — ours NtClose support for the handle

Ours's xam_task_schedule (xam.rs:216-288) mints a real Thread handle via state.alloc_handle_for(KernelObject::Thread { … }). The handle is a regular kernel object that ours's nt_close (exports.rs:2193-2225) release-counts identically to canary's ObjectTable::ReleaseHandle. No new infrastructure needed.

Step 4 — Fix shape

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

  • Line 33: register_export(... stub_success)register_export(... xam_task_close_handle)
  • New function xam_task_close_handle (~38 LOC) that:
    • Reads handle from ctx.gpr[3].
    • Checks state.objects.contains_key(&handle) — if absent, returns 0 (mirrors XFAILED(X_STATUS_INVALID_HANDLE) branch).
    • Otherwise performs ref-counted release inline (decrement; drop on zero, scrub async_file_handles and disarm_timer) — identical body to exports::nt_close but written inline to avoid widening the exports API for a single XAM helper.
    • Returns 1.
  • 4 new unit tests:
    • xam_task_close_handle_valid_handle_returns_one_and_releases
    • xam_task_close_handle_invalid_handle_returns_zero
    • xam_task_close_handle_respects_refcount
    • xam_task_schedule_then_close_round_trip_returns_one

Total: +158 LOC, 1 file (38 body + 120 tests).

Step 5 — Outcome

  • Main chain (tid=6 → tid=1): matched-prefix 102158 → 102164 (+6).
  • New first divergence (next target): idx 102164KeResetEvent return_value=1 vs 0 (same class as the C+7 predecessor's KeSetEvent fix; C+7 memo flagged this as deferred).
  • All other chains unchanged (no regressions).

Discipline notes

  • Reading-error #28 verification step took ~3 minutes (read canary source, confirm framing). Worth doing every single time.
  • Did not need to escalate. Fix scope was within the predicted ~10 LOC body + tests envelope; no handle-table refactor required.
  • Last-error semantics on the failure branch are intentionally not modeled — XThread::SetLastError is observation-only in this Phase A horizon (canary's emitter records dword return, not last-error). Note added in the code comment for future investigators.