fix(kernel): KRNBUG-KE-001 — real KeResumeThread per canary mirror

Replace the no-op cookie-returner with a real impl per canary
xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc:216-227
(XObject::GetNativeObject<XThread>()->Resume()). Mirrors
nt_resume_thread plumbing two functions below:
resolve_pseudo_handle -> scheduler.find_by_handle -> resume_ref.

Returns STATUS_SUCCESS if the KTHREAD-pointer-as-handle resolves,
STATUS_INVALID_HANDLE otherwise — matches canary's Resume()/!thread
return semantics.

Cascade-prediction scorecard (audit-018 -> post-fix):
- A PASS: tids 9 (entry=0x824D2878) and 10 (entry=0x824D2940)
  leave Suspended -> run prologue -> park on audio buffer-completion
  semaphores 0x828A3254 / 0x828A3230.
- B PARTIAL FAIL: NtSetEvent 667->3334; KeReleaseSemaphore=0;
  XAudioSubmitRenderDriverFrame=0.
- C FAIL (predicted 2->1, actual 2->2): both ExTerminateThread +
  KeReleaseSemaphore still canary-only.
- D FAIL: gamma-cluster blocker unchanged — pc-probe at
  0x82184318/0x82184374 no fires; dump-addr 0x828F4070 no DUMP;
  signal_attempts on 0x1004/0x100c/0x1020/0x15e4 still 0.

Necessary-but-not-sufficient: workers unsuspend but park on a
downstream gate that's part of the audit-009/-016/-017 gamma cluster.

Tests 600 -> 601 (+ke_resume_thread_unblocks_suspended_worker).
Lockstep instructions=100000003 imports=987516 deterministic x2.
Goldens re-baselined: sylpheed_n50m.json instructions
50000003->50000011, imports 407255->407247.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-06 20:46:46 +02:00
parent 7ed6192b7b
commit 76dfe7fd7a
4 changed files with 311 additions and 8 deletions

View File

@@ -3656,11 +3656,16 @@ fn nt_yield_execution(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut Ker
}
fn ke_resume_thread(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
// r3 = thread_ptr (KTHREAD). We don't track KTHREAD ↔ HW mapping through
// guest memory addresses, so accept and succeed. Real NtResumeThread
// below handles the handle-based path properly.
ctx.gpr[3] = 0;
let _ = state;
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
match state.scheduler.find_by_handle(handle) {
Some(r) => {
state.scheduler.resume_ref(r);
ctx.gpr[3] = STATUS_SUCCESS;
}
None => {
ctx.gpr[3] = STATUS_INVALID_HANDLE;
}
}
}
fn nt_resume_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
@@ -3983,6 +3988,52 @@ mod tests {
assert_eq!(ctx.gpr[3], 7);
}
/// `KeResumeThread` resolves the KTHREAD-pointer-as-handle, decrements the
/// target's suspend count, and unblocks once it hits zero. Mirrors
/// xboxkrnl_threading.cc:216-227 (XObject::GetNativeObject<XThread> +
/// thread->Resume()).
#[test]
fn ke_resume_thread_unblocks_suspended_worker() {
use xenia_cpu::scheduler::{BlockReason, HwState, SpawnParams};
let (mut ctx, mut mem, mut state) = fresh();
let pcr_base = SCRATCH_BASE + 0x500;
let params = SpawnParams {
entry: 0x8200_0000,
start_context: 0,
stack_base: 0x7200_0000,
stack_size: 0x10000,
pcr_base,
tls_base: 0,
thread_handle: 0x2000,
guest_tid: 42,
create_suspended: true,
is_initial: false,
tls_slot_count: 0,
affinity_mask: 0b0000_0010,
priority: 0,
ideal_processor: None,
};
state
.scheduler
.spawn(params, &mut crate::state::GuestMemoryPcr(&mut mem))
.unwrap();
let r = state.scheduler.find_by_handle(0x2000).expect("spawned");
assert_eq!(
state.scheduler.thread(r).state,
HwState::Blocked(BlockReason::Suspended)
);
ctx.gpr[3] = 0x2000;
ke_resume_thread(&mut ctx, &mem, &mut state);
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
let r = state.scheduler.find_by_handle(0x2000).expect("still alive");
assert_eq!(state.scheduler.thread(r).state, HwState::Ready);
ctx.gpr[3] = 0xDEAD_BEEF;
ke_resume_thread(&mut ctx, &mem, &mut state);
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
}
/// The regression we're guarding against: Sylpheed parks a thread on the
/// event it handed to `NtReadFile`. Historically our HLE ignored r4 and
/// left the event unsignaled — the wait never released. Completion must