# 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`: ```cpp 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`: ```cpp uint32_t NtClose(uint32_t handle) { return kernel_state()->object_table()->ReleaseHandle(handle); } ``` `xenia-canary/src/xenia/kernel/util/object_table.cc:189-208`: ```cpp 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`: ```rust 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 **102164** — `KeResetEvent 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.