Repository navigation
fix(libkernel): reject exception redirects during thread teardown - #1802
Closed
Sanjaysrinivas wants to merge 11 commits into
Closed
Sanjaysrinivas wants to merge 11 commits into
Sanjaysrinivas wants to merge 11 commits into
Conversation
A raised exception redirects the target thread to a stub that runs the handler and returns through RtlRestoreContext. On Windows 10 (19045) RtlRestoreContext restores in user mode only when the context flags are exactly CONTEXT_FULL | CONTEXT_SEGMENTS (CONTEXT_XSTATE aside); for the CONTEXT_FULL | CONTEXT_FLOATING_POINT context it was given, it calls NtContinue. When the next raise suspended the thread inside that system call, NtContinue then replaced the redirected context with the one it was restoring, so the handler never ran although sceKernelRaiseException had returned 0. guest_raise_exception lost such a delivery to its busy thread in about one run in five and hung until the ctest timeout. The context is now read with CONTEXT_SEGMENTS, so the handler returns through the user-mode restore, and a raise that finds the thread inside NtContinue or NtContinueEx (an APC or an exception handler returning) resumes it and suspends it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EH exceptions The new phase's vectored handler continues every exception the thread raises, so the thread spends much of its time in NtContinue, where a redirect is lost unless the raise retries. Without the retry the phase hangs in every run; with it the test passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elivery SuspendThread returns before the thread has stopped, and GetThreadContext waits until it has. waitCount was read right after SuspendThread, so a thread that was leaving its wait could still decrement it and drain its APCs with NtTestAlert before QueueUserAPC queued the delivery, which then waited for an alertable wait that never came. The context is now read first, so waitCount is read from a stopped thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hreads MinGW's thread_local goes through emutls and winpthreads' pthread_getspecific, which holds a per-thread spinlock. A raise that redirected a thread holding it ran the handler on that same thread, and a handler reading any thread_local, scePthreadSelf for one, spun on the lock forever. A raise that finds the target inside libwinpthread-1.dll now resumes it and suspends it again, as one inside NtContinue does. guest_raise_exception's new phase raises at a thread that calls scePthreadSelf in a loop and checks that the handler sees its own thread. It hung in every run without the retry. TechnicalDebt records that a handler can still reenter other host code the thread was interrupted in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inpthreads The retry that waits for a target to leave NtContinue, NtContinueEx or libwinpthread-1.dll had no bound, so a target that stayed there made sceKernelRaiseException spin forever. It now throws after 1 s. A new guest_raise_exception phase keeps a thread inside NtContinue by continuing a context that calls NtContinue again, and checks that the raise throws and that the handler did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceKernelRaiseException threw on Linux, so a title whose garbage collector stops threads with it (Unity's PS5Util raises SIGUSR1 on each thread and reads mc_rsp from the handler's context) could not run there. The raise now sends the host SIGUSR1 to the target thread with pthread_kill. The host handler, installed on first use with SA_SIGINFO and SA_RESTART, builds the FreeBSD-layout ucontext from the interrupted registers and the FXSAVE area, calls the guest handler on the target's own stack and writes the registers back, so the handler's changes reach the interrupted code, as they do on Windows. errno is left as the handler leaves it, as libthr does. A raise on another thread takes the target's join lock with the host signal blocked and returns ESRCH once the thread has finished, so a detached thread's pthread_t is never signalled after it exits. The pthread_t is recorded when the thread is created or adopted. Handlers are now lock-free atomics, as the host handler reads them. guest_raise_exception now runs on every platform. It also checks that r13 and xmm8 reach the handler and that r12 and xmm8 written by the handler reach the interrupted code. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwJx7AHrSUaMGSpEGhL1Kb
…ndler's MXCSR Two defects found in review of the Linux delivery: A raise right after scePthreadCreate reached the new thread before RunThread set currentThread, so scePthreadSelf() in the handler adopted a second handle, and could deadlock on glibc's thread lock when the signal interrupted pthread_getattr_np. The creator now blocks all signals around the std::thread construction, so the child starts with them blocked, and RunThread restores the creator's mask once the thread is registered, as libthr does. A raise in that window stays pending and is delivered just before the entry runs. A handler that set reserved MXCSR bits made rt_sigreturn fail with SIGSEGV. The MXCSR the handler leaves is now masked with the frame's MXCSR_MASK (0xffbf when it reads 0), as FreeBSD's fpusetregs does, on both platforms; Windows also keeps CONTEXT.MxCsr equal to FltSave.MxCsr. guest_raise_exception raises right after each of 50 thread creations and checks scePthreadSelf() in the handler, and its context round trip sets a reserved MXCSR bit and FZ and checks that only FZ is kept. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwJx7AHrSUaMGSpEGhL1Kb
Recheck guest completion after Windows suspension takes effect and classify suspension/context failures during termination as ESRCH. Otherwise a late handler can adopt a different guest thread handle. Cover address-wait recovery and exit races on Windows and Linux; document the remaining host-lock and coarse-wait limitations. Refs boykopovar#1668
This was referenced Oct 8, 2026
This was referenced Oct 8, 2026
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
|
9 tasks done
Sanjaysrinivas
marked this pull request as ready for review
October 8, 2026 21:38
9 tasks done
9 tasks done
This was referenced Oct 9, 2026
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fix two Windows exception/exit races from #1668. A target can finish after the initial
sceKernelRaiseExceptioncheck but before suspension takes effect. Redirecting it after guest-thread unregistration makes the handler'sscePthreadSelf()adopt a different handle. A terminating native thread can also reject suspension before its Windows handle is signaled, causing a runtime exception instead ofESRCH.The Windows path now checks guest completion as well as native exit, rechecks
_finishedafterGetThreadContexthas stopped the target, and treats context-read failures during completion asESRCH. Other host API failures retain their diagnostics.Add
guest_exception_wait_recoverywith Release-active requirements, atomic gates, five-second progress checks and a 20-second CTest timeout. Each run covers:The address-wait tests include the wait-entry race. Publishing a changed value before wake makes both registration orderings valid; these tests do not claim every raise interrupts an already enrolled host wait. Independent raiser/waker threads let the coordinator release a held handler without waiting on host locks it may retain. The raiser is a guest thread and is joined, avoiding leaked adopted host-thread handles.
Update the existing architecture documentation with the delivery flow, rejection ordering, wait invariants, test matrix and support boundaries. Update TechnicalDebt for the remaining teardown and host-lock limits.
Dependencies and coordination
Depends on #1431 at
77143010efe1555449b6f11ee6a91b1517fd4b6dfor Linux delivery.Depends on #1421 at
24dc98d7c035bde08e679e3c62a59402a2b57cc5for Windows delivery robustness. The new tests exposed MinGW TLS failures without it.Based on upstream
mainat2cc7f09d1d3c90fa3daf4ba2c5b170b036361770. Both prerequisite histories and authors are preserved through merges. Their test-file conflict is resolved by retaining Linux register/startup coverage, guarding Windows-only SEH/continuation cases, and correcting delivery-count offsets for the combined phases. The Linux MXCSR handling and Windows retry/context changes are both retained. The final main refresh adds upstream image-by shader support and leaves the tested kernel source unchanged.The new change is commit
694a126b, five files. #1562 also changes Windows delivery; its alternative implementation is not stacked here. #1652 changes signal-mask handling, which this follow-up does not implement. #1431's current published head has two commits; this PR does not assume the later mask work described in its body is present.The overlap bot also flags #1746, which replaces the Linux thread-start promise and preserves configured stack bounds. Its overlap is with startup code included from #1431, not the new Windows teardown guard. Neither #1562 nor #1746 is an additional prerequisite for this PR. Whichever merges second must reconcile the shared code: retain Linux signal masking and native-handle publication alongside #1746's startup changes, and preserve the finished-target rejection if Windows delivery is replaced by #1562. Rerun exception, thread creation and stack tests after integration.
Related cancellation coverage is in #1739. This PR addresses part of workstream B and leaves #1668 open.
Tested
Local Release builds use the production libc/libkernel CMake source lists and test registrations through a small untracked validation parent. They do not replace production functions or build unrelated graphics/relinker dependencies.
That is 6,000 address-wait cases and 6,400 exit races per host. The existing
guest_exception_handler,guest_raise_exceptionandguest_sync_on_addresstests also pass three repetitions per host. No title, console or GPU behavior was tested.Full GitHub CI passes on combined head
0623b090f8955b68a8e481a6df4cf42bda82b5c4: Linux, Windows, conventions, and progress report. The relinker job is skipped by the path filter.Before the new guard, the regression failed the target-handle identity requirement. With only the post-suspension check, further stress reproduced
cannot suspend the target threadduring teardown; consulting guest completion in failure paths fixes that window too.Two temporary Linux mutations were caught: removing address-wake notification failed bounded progress; forcing successful address-wait returns failed the timeout requirement. Production sources were restored byte-for-byte and ten restored runs passed. Conventions, whitespace and native test-registration checks pass.
Standard checkout reproduction:
Remaining limits and reproduced evidence
#1488's host-lock mechanism is reproduced on Windows. A preliminary version kept the handler waiting while the coordinator synchronously called
sceKernelSyncOnAddressWake. GDB showed the target paused inHandler, interrupted beneathRtlAllocateHeapduring address-wait enqueue, and the coordinator blocked in winpthreads fromsceKernelSyncOnAddressWakeon the address-queue mutex. The handler's release depended on that wake returning. This is documented, not fixed by the teardown guard; a general host-critical-section deferral design is still needed.Windows coarse address-wait/delivery races also failed to complete reliably in the preliminary matrix. Their root cause remains unresolved. This CTest explicitly unsets
APS5_COARSE_TIMED_WAITSon Windows and claims default alertable-wait coverage only. Linux uses its existing condition-variable path.Signal masks, teardown delivery-completion guarantees, stale non-null handles, full console context equivalence and Windows upper-YMM preservation remain unverified or unsupported. Accepted raises are not counted as executed handlers during exit races.
Checklist
main; checked overlapping work and explicitly listed prerequisites.