Repository navigation
fix(libkernel): reject sceKernelRaiseException targets whose thread was released on Linux - #16380
hugodearaujo wants to merge 2 commits into
Conversation
…as released on Linux sceKernelRaiseException used its thread handle without checking that it still named a guest thread. It read thread->_finished, and on Linux RaiseOn then locked thread->_join_mtx and called pthread_kill(thread->hostThread). A PthreadPrivate is deleted when a detached thread exits and when a joined thread's handle storage is reused by the next scePthreadCreate, so a raise at such a handle reads and locks freed memory and signals a host thread that may be gone. Every other libkernel call that takes a guest thread handle looks it up in the thread registry before touching it: guest pthread_kill, scePthreadCondSignalto and pthread_setaffinity_np through GuestThreadIsLive, scePthreadAttrGet and the priority and scheduling calls through LockLiveGuestThread, scePthreadJoin and scePthreadDetach through LockRegisteredGuestThread. sceKernelRaiseException was the one call that skipped the lookup. Return ESRCH unless the thread is registered and not finished. On Linux, keep the registry lock while RaiseOn takes _join_mtx and sends the signal, in the order scePthreadJoin and scePthreadDetach use, so the thread cannot be released between the check and pthread_kill. A raise at the calling thread still sends directly. The new guest_raise_released_thread test keeps the released PthreadPrivate zero-filled instead of freeing it and raises at a joined thread whose storage was reused and at detached threads that exited. Without this change it dies with SIGSEGV; with it every raise returns ESRCH. Found while looking into a SIGSEGV 37 s into a boot of God of War Sons of Sparta (PPSA28997), whose IL2CPP collector raises at every thread it has registered and skips a thread when the raise returns ESRCH. That crash has not reproduced since and is not tied to this defect.
|
Windows check of head Setup: Windows 11 Enterprise 22621, WinLibs GCC 15.2 UCRT, Release. The hosted Windows check does not run these tests on pull requests.
AI-assisted: yes. |
|
ASan + UBSan check of head Setup: Ubuntu 26.04 container, GCC 15.2.0, the Sanitizers workflow's flags ( Result: the test fails 3 of 3 runs with an Cause: the test replaces Change that fixes it: the two nothrow overloads next to the existing ones in the test: void* operator new(std::size_t bytes, const std::nothrow_t&) noexcept { return std::malloc(bytes != 0 ? bytes : 1); }
void* operator new[](std::size_t bytes, const std::nothrow_t&) noexcept { return std::malloc(bytes != 0 ? bytes : 1); }With them the test passes 3 of 3 runs and the sanitizers report nothing. Other control: with the test as it is in this PR and Once this is merged the test would show up as a new failure in the nightly Sanitizers job. AI-assisted: yes |
…hread The test replaces operator new and the operator delete overloads, and its delete frees with std::free, but it left the std::nothrow_t forms of operator new to the runtime. scePthreadAttrInit allocates with new (std::nothrow), so under AddressSanitizer the attribute came from ASan's allocator and went back to free through the test's delete, which ASan reports as alloc-dealloc-mismatch. Allocate the nothrow forms with std::malloc too, as boykopovar#15463 does for guest_cnd_at_thread_exit.
|
@StepFPV thanks, confirmed and fixed in 99af0d9 with the two nothrow overloads you suggested. Local check with an
Release: the 15 |
What
sceKernelRaiseExceptionuses its thread handle without first checking that the handle still names a guest thread:thread->_finished;RaiseOnthen locksthread->_join_mtxand callspthread_kill(thread->hostThread, SIGUSR1).A
PthreadPrivateis deleted in two cases:ThreadGuard→ReleaseThread);scePthreadCreatereuses its handle storage (RecycleJoinedThread).A raise at either handle is a use after free. It reads and locks freed memory, then signals a host thread that may no longer exist.
Why this is a defect even without a title that reproduces it
Every other libkernel call that takes a guest thread handle looks it up in the thread registry before touching it:
pthread_kill,scePthreadCondSignalto,pthread_setaffinity_npGuestThreadIsLive, ESRCH otherwisescePthreadAttrGet,pthread_attr_get_np,pthread_getprio, the scheduling calls,ResumeGuestThread_nid_no_patchLockLiveGuestThreadscePthreadJoin,scePthreadDetachLockRegisteredGuestThread, and_join_mtxis taken only under itsceKernelRaiseExceptionis the one call that skips the lookup. It is also the closest sibling of guestpthread_kill, which does the check.Titles also count on ESRCH here for a thread that is gone. The Boehm collector in IL2CPP titles raises at every thread it has registered and skips a thread when the raise fails. For example,
GC_suspend_allin God of War Sons of Sparta'sIl2cppUserAssemblies.prx:sceKernelRaiseException(thread, 30)throughPS5Util.prx;0x80020003(ESRCH) or0x80020016.A title reaches the bug only when it raises at a handle whose thread was already released, which depends on thread lifetimes and timing. That is why it is hard to observe in a title. The defect is in the code either way, and it is the same check the neighbouring calls already make.
Change
sceKernelRaiseExceptionreturnsSCE_KERNEL_ERROR_ESRCHunlessGuestThreadIsLive(thread)(registered and not finished), so a released handle is never dereferenced.RaiseOnholdsLockLiveGuestThread(thread)while it takes_join_mtxand sends the signal, so the thread cannot be released between the check andpthread_kill:_join_mtx, the orderscePthreadJoinandscePthreadDetachuse;Sendonly callspthread_killand does not wait for the delivery;RaiseOnis unchanged.Where it came up: God of War Sons of Sparta (PPSA28997)
mainf2d874c, one of the first nine boots died with SIGSEGV (SEGV_MAPERR) on the main thread, 37 s in.Storage: none), so there is no stack.main04a9eb4, with a warm shader cache, with it disabled, and pinned to 3 CPUs;So I don't know yet how to reproduce it in a title, and I can't say this defect caused that crash. This PR stands on the defect itself.
Tested
Arch Linux 7.2.11, GCC, Release with
BUILD_TESTING=ON, onmain1ff0fe9.guest_raise_released_threadtest (Linux).operator deleteso that the watchedPthreadPrivateis zero-filled and kept instead of freed. That makes a read through the stale handle deterministic instead of depending on what the heap did with the memory.scePthreadCreatereused;main'sException.cppfails with***Exception: SegFaultin 5 of 5 runs. The zeroed_finishedreads false, andpthread_killgets host thread 0. With the change it passes in 50 of 50 runs.ctest -R "raise|guest_exception_handler|released_handle|handle_after_exit|final_release|sigaltstack|sigtimedwait": all 15 tests pass.tools/check_conventions.py --base origin/main: passes.-DANYPS5_ENABLE_ASAN=ON,-g1 -fsanitize=undefined -fno-sanitize=vptr,ASAN_OPTIONS=detect_leaks=0):guest_raise_released_threadpasses 3 of 3 with no report. The first version of the test did not define the nothrowoperator newoverloads and failed there withalloc-dealloc-mismatch(found by StepFPV below, fixed in 99af0d9).main.Help wanted
If a title is needed as evidence before this can merge, that's where I'd appreciate help:
[exception] guest context signal=0x1elines right before the crash), please run it with and without this change and post the crash counts.guest_raise_exceptionandguest_raise_released_threadwould help. The second is Linux-only for now, because it relies on glibc'smalloc_usable_size.Checklist
main; no other open PR implements the same functions (feat(libkernel_sys): implement pthread_suspend_np #12130 also editssceKernelRaiseException, in its Windows suspended branch; the two changes touch different lines)NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)