Skip to content

fix(libkernel): reject exception redirects during thread teardown - #1802

Closed
Sanjaysrinivas wants to merge 11 commits into
boykopovar:mainfrom
Sanjaysrinivas:test/exception-address-wait
Closed

Sanjaysrinivas wants to merge 11 commits into
boykopovar:mainfrom
Sanjaysrinivas:test/exception-address-wait

Conversation

@Sanjaysrinivas

@Sanjaysrinivas Sanjaysrinivas commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

Fix two Windows exception/exit races from #1668. A target can finish after the initial sceKernelRaiseException check but before suspension takes effect. Redirecting it after guest-thread unregistration makes the handler's scePthreadSelf() adopt a different handle. A terminating native thread can also reject suspension before its Windows handle is signaled, causing a runtime exception instead of ESRCH.

The Windows path now checks guest completion as well as native exit, rechecks _finished after GetThreadContext has stopped the target, and treats context-read failures during completion as ESRCH. Other host API failures retain their diagnostics.

Add guest_exception_wait_recovery with Release-active requirements, atomic gates, five-second progress checks and a 20-second CTest timeout. Each run covers:

  • 60 address-wait cases across the unsuffixed, 8-, 16-, 32- and 64-bit APIs: wake requested before/after handler release, timeout with a wake at another address, handler identity/stack, original join result, and subsequent address reuse.
  • 64 exception/exit races: one acknowledged handler before exit is released, then a raise racing teardown; count accepted/rejected requests and observed handler calls separately, verify target identity, reject a finished but still joinable handle, and retain the original thread result.
  • Null targets and unsupported raise values.

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 77143010efe1555449b6f11ee6a91b1517fd4b6d for Linux delivery.

Depends on #1421 at 24dc98d7c035bde08e679e3c62a59402a2b57cc5 for Windows delivery robustness. The new tests exposed MinGW TLS failures without it.

Based on upstream main at 2cc7f09d1d3c90fa3daf4ba2c5b170b036361770. 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.

Host Toolchain Final regression result
Windows 11 Pro, build 26300 WinLibs r7 MinGW-w64 GCC 15.2.0, C++20 100/100, 25.64 s
Ubuntu 24.04 under WSL2, kernel 6.6.87.2 GCC 13.3.0, C++20 100/100, 24.39 s

That is 6,000 address-wait cases and 6,400 exit races per host. The existing guest_exception_handler, guest_raise_exception and guest_sync_on_address tests 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 thread during 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:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
cmake --build build --target guest_exception_wait_recovery_tests guest_raise_exception_tests guest_exception_handler_tests guest_sync_on_address_tests
ctest --test-dir build --output-on-failure -R '^guest_(exception_wait_recovery|raise_exception|exception_handler|sync_on_address)$' --repeat until-fail:3 --parallel 4
ctest --test-dir build --output-on-failure -R '^guest_exception_wait_recovery$' --repeat until-fail:100

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 in Handler, interrupted beneath RtlAllocateHeap during address-wait enqueue, and the coordinator blocked in winpthreads from sceKernelSyncOnAddressWake on 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_WAITS on 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

popcoder565 and others added 10 commits October 7, 2026 23:48
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
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Progress report (2cc7f09 → 0623b09)

📈 System libraries: 84.79% (+0.03%, +1 implemented functions)

✅ 1 implemented
Library Name
libkernel sceKernelRaiseException

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

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.

Pull request Conflicting files (lines on main) Same exports Same new files
#1988 core/libs/prx/libkernel/Pthread/include/Pthread.hpp L14-15, L82
core/libs/prx/libkernel/Pthread/src/Thread.cpp L20-21, L317-335
#2001 core/libs/prx/libkernel/CMakeLists.txt L292-321
core/libs/prx/libkernel/Pthread/src/Thread.cpp L180-181, L420-421
core/libs/prx/libkernel/System/src/Exception.cpp L5-267, L272-275, L280-282, L287-296
core/libs/tests/GuestRaiseException.cpp L1-297
docs/dev/ARCHITECTURE.md L1-69
docs/dev/TechnicalDebt.md L239-254
#2051 core/libs/tests/GuestRaiseException.cpp L283

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: no title hits this teardown race, and the branch merges #1421 and #1431 in instead of building on main. If a title hits it once those land, please reopen rebased on main, without merge commits or the ARCHITECTURE.md section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants