Skip to content

feat(libkernel): deliver sceKernelRaiseException on Linux - #1431

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
Zaid-Talib:claude/focused-turing-ivfmjn
Oct 11, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
Zaid-Talib:claude/focused-turing-ivfmjn

Conversation

@Zaid-Talib

@Zaid-Talib Zaid-Talib commented Oct 8, 2026 •

Copy link
Copy Markdown

What

sceKernelRaiseException threw NotImplemented on Linux, so a title whose garbage collector stops threads with it (Unity's PS5Util module: installs a SIGUSR1 handler with sceKernelInstallExceptionHandler, raises it on each thread and reads mc_rsp from the context) could not run there. Two commits:

  1. feat(libkernel): deliver sceKernelRaiseException on Linux: the raise sends the host SIGUSR1 to the target thread with pthread_kill (model: shadPS4 exception.cpp, a sigaction wrapper with SA_SIGINFO | SA_RESTART). The host handler builds the FreeBSD-layout ucontext the Windows path already uses (general registers, rip, rsp, rflags, cs, ss, 512-byte FXSAVE area), calls the guest handler on the target's own stack and writes the registers back. Only the 416 bytes up to XMM15 are written back: the rest of the FXSAVE area holds the kernel's XSAVE frame marker. A raise on another thread blocks the host signal, takes the target's join lock and returns ESRCH once the thread has finished. The handler table is lock-free because the host handler reads it.
  2. fix(libkernel): keep raises off unregistered threads and clamp the handler's MXCSR: scePthreadCreate blocks all signals around pthread_create and RunThread restores the creator's mask once the thread is registered, so a raise right after creation cannot reach a thread whose scePthreadSelf() would adopt a second handle. The MXCSR a handler leaves is masked with MXCSR_MASK (0xffbf when it reads 0), as FreeBSD's fpusetregs does, on both platforms; without it a reserved bit made rt_sigreturn fail with SIGSEGV.

guest_raise_exception is now registered on every platform. TechnicalDebt: the "throws on Linux" line is replaced by what still differs from the console (the handler runs wherever the thread was interrupted, host waits resume, calls Linux does not restart return early or EINTR).

Tested

Rebased onto main ac2c5c0 (the reviewer resolved the conflicts with the pthread_create thread start and the Windows-only test). Linux, GCC, libkernel scope: guest_raise_exception passes 20/20 in a row, guest_signals, guest_exception_handler and the guest_pthread_* tests pass; the new test fails without the source changes. Covers: self, busy, waiting in sceKernelWaitSema, blocked on a host mutex, leaving a wait; register round trip (r12, r13, xmm8, MXCSR including a reserved bit); 50 raises right after scePthreadCreate checking scePthreadSelf() in the handler; a finished thread returns ESRCH.
Third parties: Hollow Knight: Silksong (PPSA12544, Arch, RX 9070 XT, @hugodearaujo) gets past sceKernelRaiseException not implemented; God of War Sons of Sparta (PPSA28997, Arch, RTX 3050, @GustavoGamarra95) boots and plays several levels with this branch and #1482. Windows: not run here; @popcoder565 ran the pre-split branch on Windows 10.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

Cloud credits project contribution

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Please split this into separate PRs: the per-thread sigprocmask with FreeBSD's bit layout, sceKernelRaiseException on Linux, and the sleepUs EINTR fix in AudioOut. One topic per PR; at +773 lines across those it can't be reviewed or gated as one. For the Linux raise, the title ID and the log line where it stops on main would show the need.

@popcoder565

Copy link
Copy Markdown
Contributor

Since Windows wasn't run, I ran it there before the split, in case it helps the Windows half: Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs), Release, main bc1fbe8 merged with 24f89cf.

In the toggling hang, main waits in scePthreadJoin, and both toggling threads are stuck in the same place:

#0 libwinpthread-1.dll
#1 libwinpthread-1.dll
#2 __gthread_getspecific              gthr-default.h
#3 __emutls_get_address (obj=__emutls_v._ZL13currentThread)
#4 scePthreadSelf                     libkernel.prx

MinGW's thread_local goes through emutls and winpthreads' pthread_getspecific, which takes a per-thread spinlock (spin_keys in mingw-w64's pthread.c). _sigprocmask_nid_postfix calls scePthreadSelf() before it claims SIGUSR1's bit, so a raise can land while the thread holds that spinlock. The delivery then ends in DeliverUnblocked, which self-raises the SIGUSR1 that went pending, and RaiseOn checks thread == scePthreadSelf() on that same thread. It spins on the lock the interrupted frame holds. Any emutls thread_local read on the delivery path can do the same, for example currentThread and the TimedWait wait state.

A way out could be to identify the current thread on the delivery path without emutls: compare GetCurrentThreadId() with the target's native thread id, or pass the PthreadPrivate& that Deliver already has to a self-delivery helper instead of going through sceKernelRaiseException.

@popcoder565

Copy link
Copy Markdown
Contributor

Follow-up: the same deadlock reproduces on main without this PR, when a handler reads a thread_local while the target is interrupted inside scePthreadSelf. #1421 now retries any raise that finds its target inside libwinpthread-1.dll (384078c), so the toggling phase should stop hanging once this is rebased on it.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@Zaid-Talib
Zaid-Talib force-pushed the claude/focused-turing-ivfmjn branch from 24f89cf to 7714301 Compare October 8, 2026 15:21
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@hikazey hikazey mentioned this pull request Oct 8, 2026
9 tasks done
@johnmaia johnmaia mentioned this pull request Oct 8, 2026
9 tasks done
@oneandonlydean

Copy link
Copy Markdown
Collaborator

The branch now has only the Linux sceKernelRaiseException delivery and the MXCSR clamp, but the title and description still describe the five-commit version (per-thread sigprocmask, AudioOut). Please update them to match the diff.

@hugodearaujo

Copy link
Copy Markdown
Contributor

Also gets Hollow Knight: Silksong (PPSA12544) further on Linux (Arch, GCC, RX 9070 XT). On current main (d70b89989) with #1836 applied (without it, the title stops earlier at shader registration), it stops at:

terminate called after throwing an instance of 'std::runtime_error'
  what():  sceKernelRaiseException not implemented

With this branch merged on top as well, that raise is delivered. The title loads globalgamemanagers, unity default resources and sharedassets0, and opens level0. It then stops on an uncaught Il2CppExceptionWrapper, which looks unrelated to signal delivery. It is most likely a Unity plugin that cannot start on Linux yet (#846).

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for splitting this. Two things before it can be reviewed and merged:

  • The branch conflicts with main in core/libs/tests/GuestRaiseException.cpp (#1478 changed it). Please rebase and rerun guest_raise_exception and guest_signals.
  • The title and description still describe the five-commit version (per-thread sigprocmask, AudioOut sleepUs, 409 ctest results, stress runs, mutation checks for code that is no longer here), as @oneandonlydean asked on Oct 9. Please rewrite them for the current diff: Linux sceKernelRaiseException delivery, the startup signal block in scePthreadCreate, and the MXCSR clamp. Update "Tested" with what you ran on this head, and the list of overlapping PRs.

Useful for the description: the Silksong (PPSA12544) log @hugodearaujo posted, where main stops at sceKernelRaiseException not implemented.

@GustavoGamarra95

Copy link
Copy Markdown
Contributor

Another title this gets further on Linux: God of War Sons of Sparta (PPSA28997, Unity IL2CPP). Its GC stops threads with sceKernelRaiseException, so on main it stops with sceKernelRaiseException not implemented right after creating its job workers. With this branch (head 7714301) the raise is delivered, and the title boots and plays through several levels on Arch Linux (RTX 3050), together with #1482.

To test it I had to rebase it onto main 942a174 myself. The conflicts included Pthread.hpp, Exception.cpp, Thread.cpp and GuestRaiseException.cpp:

  • Thread.cpp: main now creates guest threads with pthread_create instead of std::thread. I blocked all signals around pthread_create, restored the creator's mask on both the success and the failure path, and set the saved mask in RunThread as the branch does.
  • GuestRaiseException.cpp: I did not merge it, because main added Windows checks in the same places as the branch's Linux ones.

I can share the resolved diff if it helps.

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
@adriadam10 adriadam10 changed the title feat(libkernel): deliver guest signals on Linux and keep signal masks per thread feat(libkernel): deliver sceKernelRaiseException on Linux Oct 11, 2026
@adriadam10
adriadam10 force-pushed the claude/focused-turing-ivfmjn branch from 7714301 to c356112 Compare October 11, 2026 09:20

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current main (c356112) and fixed the two points from the earlier review ourselves:

  • Conflicts resolved: guest threads now start with pthread_create, so the startup signal block wraps that call and RunThread restores the creator's mask; GuestRaiseException.cpp now runs on every platform, with the Windows-only parts under #ifdef _WIN32; removed the duplicated handlerSelf.
  • Title and description rewritten for the current diff (Linux delivery, startup signal block, MXCSR clamp), with the Silksong and God of War: Sons of Sparta reports.

Checked: guest_raise_exception 20/20 in a row, guest_signals, guest_exception_handler and guest_pthread_* green on Linux; the test fails without the source changes. Windows not run here.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 11, 2026
@github-actions

Copy link
Copy Markdown

Progress report (e272b92 → c356112)

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

✅ 1 implemented
Library Name
libkernel sceKernelRaiseException

@adriadam10
adriadam10 merged commit 09bd4aa into boykopovar:main Oct 11, 2026
12 checks passed
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.

7 participants