Skip to content

fix(libkernel): keep the signal mask per thread - #3986

Open
Silverash-i8 wants to merge 1 commit into
boykopovar:mainfrom
Silverash-i8:fix/per-thread-signal-mask
Open

Silverash-i8 wants to merge 1 commit into
boykopovar:mainfrom
Silverash-i8:fix/per-thread-signal-mask

Conversation

@Silverash-i8

Copy link
Copy Markdown
Contributor

What

Fixes #2879. sigprocmask, _sigprocmask and pthread_sigmask shared one process-wide mask that kept only the first 32 signals. A thread changing its mask changed every thread's, the upper words read back as zero, SIGKILL and SIGSTOP could be blocked, and a signal raised while blocked was dropped.

Following FreeBSD's kern_sigprocmask, Signals.cpp now keeps a blocked and a pending set of all 128 signals per guest thread:

  • scePthreadCreate starts the new thread with its creator's mask.
  • SIGKILL and SIGSTOP are removed from any mask that is set.
  • A raise (and so kill and pthread_kill to self) of a blocked signal stays pending and is delivered before sigprocmask returns from the unblock. A signal whose disposition is ignore is discarded instead of kept pending.

guest_signals checks a child thread inheriting its creator's mask, changing its own and leaving the creator's alone, a full-width round trip, the SIGKILL/SIGSTOP bits dropped, the deferred handler running on unblock, and an ignored blocked signal not running a handler later. The test it replaces expected the signal raised while blocked to be lost.

The TechnicalDebt entry for the shared mask is replaced with the limitation left: a host signal from outside the guest (Ctrl+C, or SIGTERM from another process) is checked against the mask of whichever host thread receives it and stays pending on that thread, where FreeBSD would deliver it to a thread that does not block it.

Tested

On main 0a8b1d32:

  • Windows 11 Home 26200, WinLibs GCC 15.2.0 r7, Release: guest_signals, guest_signal_sets, guest_user_context and guest_user_context_exit pass 5 of 5 (--repeat until-fail:5); sigsetjmp/siglongjmp and getcontext/setcontext go through _sigprocmask. All tests except agc_driver*: 540 of 540.
  • Linux (WSL2 Ubuntu 22.04, glibc 2.35, GCC 13.4.0, Release): the same four tests pass 5 of 5. All tests except agc_driver*: 533 of 535; the two failures come from the WSL environment and do not reach AnyPS5 code (pr_overlap_encoding uses subprocess._text_encoding, which Python 3.10 lacks, and linux_executable_exports stops compiling its own C probe, RTLD_DEFAULT undeclared). Not run on native Linux.
  • Built against main's Signals.cpp and Thread.cpp, the new guest_signals aborts on both systems (checked before the rebase onto 0a8b1d32, on a11c8f7f).
  • tools/check_conventions.py --base upstream/main passes.
  • Not tested: titles.

#3800 (setitimer) reads the old process-wide blockedMask in ExpireAlarm, which this change removes, so whichever of the two lands second needs a small adaptation.

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 (Claude Code)

https://claude.ai/code/session_012SLQbhAiexxW6TtjYToW8a

sigprocmask, _sigprocmask and pthread_sigmask shared one process-wide
mask of the first 32 signals, so a thread changing its mask changed
every thread's, the upper words read back as zero, SIGKILL and SIGSTOP
could be blocked, and a signal raised while blocked was dropped (boykopovar#2879).

Following FreeBSD's kern_sigprocmask, each guest thread now has its
own 128-signal blocked and pending set. A new thread starts with its
creator's mask, SIGKILL and SIGSTOP are removed from any mask that is
set, and a raise of a blocked signal stays pending and is delivered
when the thread unblocks it. An ignored signal is discarded rather than
kept pending.

Claude-Session: https://claude.ai/code/session_012SLQbhAiexxW6TtjYToW8a
@Kikoarl

Kikoarl commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Tested on native Linux (CachyOS x86_64, GCC) with an NVIDIA GeForce RTX 5060 Ti:

  • guest_signals: ran 10 consecutive stress runs (--repeat until-fail:10) — 10/10 passed (15.05s total).
  • guest_signal_sets & guest_user_context_exit pass clean.
  • Verified in live title execution (Astro Bot PPSA21567): 70+ worker threads spawned via scePthreadCreate correctly inherit masks with zero deadlocks or regressions.
  • tools/check_conventions.py passes clean.

Validates the fix on native Linux and title execution as requested in the description. LGTM!

@oneandonlydean

Copy link
Copy Markdown
Collaborator

One thing before merging: please drop the comment at core/libs/prx/libkernel/System/src/Signals.cpp:47-48. Code comments aren't allowed apart from technical-debt markers; the TechnicalDebt entry already says it.

@Dextroid17 Dextroid17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The control is what the record above does not have, and the overlap the bot lists is a text conflict rather than a same-file warning. Head 066b18da0, parent 0a8b1d32b, in a worktree:

head      guest_signal + guest_signal_sets → 100% tests passed out of 2, exit 0
control   parent's Pthread/src/Thread.cpp + System/src/Signals.cpp, the PR's tests kept
          → the test process exits 134 (SIGABRT, "Aborted (core dumped)"),
            ctest: 691 - guest_signals (Subprocess aborted), 50% passed, 1 failed out of 2 (ctest exit 8)

The abort is deterministic and comes from the parent's own bookkeeping: it wrote previousSet->bits[1..3] = 0, while the new test asserts the previous mask above bit 31 comes back set (previous.bits[1] == 0x1, with bit 32 reaching bits[2] and bits[3]). That is the half a green head cannot show.

Two checks on the change itself:

  • The mechanism fits this port. GuestSignalSet has held std::uint32_t bits[4] (Signals.cpp:24) since before this change; what was wrong is the mask bookkeeping — a single std::atomic<std::uint32_t> blockedMask, the operation applied to set->bits[0] only, and bits[1..3] returned as zero, so a mask above 32 was dropped and never read back. The change keeps the blocked and pending sets per thread and applies the operation to all four words. Per-thread storage is meaningful here because each guest thread runs on its own host thread: pthread_create(&p->hostThread, …, StartNativeThread, …) on the POSIX path (Thread.cpp:586), _beginthreadex on the Windows path (Thread.cpp:549), the entered thread adopted through pthread_self() (Thread.cpp:692).
  • On the overlap, the bot's table is a same-file, same-line-range warning computed against main and says nothing about whether the two branches can merge; the measured state is that they cannot be merged without a decision. git merge-tree --write-tree pr-3986 pr-4138 exits 1 with CONFLICT (content) in core/libs/prx/libkernel/System/src/Signals.cpp, in three regions — the blocked check in Dispatch, raise_nid_postfix, and _sigprocmask_nid_postfix — where both branches add the same three how values under differently capitalised names (GuestSigBlock/GuestSigUnblock/GuestSigSetmask against GuestSigblock/GuestSigunblock/GuestSigsetmask, the second side also adding GuestEintr, GuestEagain and GuestSiuser) and both add pending-set handling. pr-3986 with pr-4383 and pr-4138 with pr-4383 produce trees with no textual conflict (exit 0; neither result was built or run, so that is all it shows). #4138's body already says it will keep this PR's per-thread masks and drop its own duplicate pending handling, so the conflict makes that rebase required rather than optional, and #4383 can go either side of the pair without cost. A line recording that would spare the next reader the experiment.

The comment oneandonlydean asked to be dropped sits at Signals.cpp:46-47 on this head (his range says 47-48); nothing above depends on it — and if the overlap is worth recording in the repository, docs/dev/TechnicalDebt.md in the same pull request is the place for it.

Written with AI assistance.

Hermes Agent 🤖 at your service

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

A small observation on the how handling in sigprocmask / _sigprocmask: the PR keeps throw std::invalid_argument for a how other than SIG_BLOCK, SIG_UNBLOCK and SIG_SETMASK. A throw out of a guest call ends the process, while FreeBSD 11.4's kern_sigprocmask writes the old mask first and then fails the unknown how with EINVAL (22). Returning EINVAL there would match it. On current main I checked this on Windows: sigprocmask with how 0, 4 and -1 and a non-null set aborts with the uncaught exception. I have a small test for it (guest_signals, how 0/4/-1) but did not open a PR, because this one rewrites the same lines; feel free to take it.

AI-assisted (Claude Code).

@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 this, the per-thread mask is the right model. It no longer applies to main, and the conflict is not only textual:

  • main now keeps a process-wide pending set in Signals.cpp (MarkPending/TakePending/PendingInSet, added with sigtimedwait in #3430, used by sigwait, sigwaitinfo, pause and killpg). The per-thread pendingSignals here is a second store for the same thing: a signal that raise leaves in it is never seen by sigwait/sigtimedwait. Please rebase and decide how the two relate (for example blocked mask per thread, pending set kept on main's structure), then cover the interaction with a test.
  • ExpireAlarm (setitimer/alarm, #3800) reads blockedMask for SIGALRM and SignalBlocked only looks at the first 32 signals. Both need to use the new per-thread mask, and the alarm thread is not a guest thread, so say which mask it checks.
  • docs/dev/TechnicalDebt.md was split per library: the entry now lives in docs/dev/debt/libkernel.md, where the old pthread_sigmask line (line 123) and the sigtimedwait line (149, which describes the process-wide pending set) both need updating.
  • @oneandonlydean's request is still open: drop the comment at Signals.cpp:47-48.
  • @xXPBSXx's point about an unknown how (write the old mask, return EINVAL instead of throw, per FreeBSD 11.4 kern_sigprocmask) touches the same lines you rewrite, so it fits in this PR.

After the rebase, guest_signals, guest_signal_sets, the sigtimedwait tests and guest_itimer should pass together, with the new guest_signals still failing on main's Signals.cpp/Thread.cpp.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 907 docs/dev/TechnicalDebt.md
PR 1368 docs/dev/TechnicalDebt.md
PR 1384 docs/dev/TechnicalDebt.md
PR 1395 docs/dev/TechnicalDebt.md
PR 1601 docs/dev/TechnicalDebt.md
PR 1835 docs/dev/TechnicalDebt.md
PR 4563 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 5021 core/libs/prx/libkernel/Pthread/src/Thread.cpp
docs/dev/TechnicalDebt.md
PR 6039 docs/dev/TechnicalDebt.md
PR 6044 core/libs/tests/GuestSignals.cpp
PR 6879 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 6893 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7149 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7201 core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/prx/libkernel/System/src/Signals.cpp
PR 7532 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7565 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7622 core/libs/prx/libkernel/System/src/Signals.cpp
PR 7804 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7838 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 7853 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8041 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8407 core/libs/prx/libkernel/System/src/Signals.cpp
core/libs/tests/GuestSignals.cpp
PR 8411 core/libs/prx/libkernel/System/src/Signals.cpp
core/libs/tests/GuestSignals.cpp
PR 8428 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8477 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8497 core/libs/prx/libkernel/System/src/Signals.cpp
PR 8592 core/libs/prx/libkernel/Pthread/src/Thread.cpp
PR 8658 core/libs/prx/libkernel/Pthread/src/Thread.cpp
core/libs/prx/libkernel/System/src/Signals.cpp
PR 8700 core/libs/prx/libkernel/System/src/Signals.cpp
PR 8866 core/libs/prx/libkernel/System/src/Signals.cpp
core/libs/tests/GuestSignals.cpp
PR 8872 core/libs/prx/libkernel/System/src/Signals.cpp
core/libs/tests/GuestSignals.cpp
PR 8890 core/libs/prx/libkernel/Pthread/src/Thread.cpp

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Conflicts with main; please rebase. Still open from 2026-10-13: @adriadam10's review above (how the per-thread mask relates to main's process-wide pending set, ExpireAlarm/SignalBlocked, the debt entry now in docs/dev/debt/libkernel.md, and the Signals.cpp:47-48 comment).

@github-actions github-actions Bot removed the stale Conflicting with main and no activity for 7 days label Oct 17, 2026
@github-actions github-actions Bot added the fix error Closes an issue (Fixes #N) label Oct 17, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 20, 2026

This branch has not been deployed

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

Labels

docs fix error Closes an issue (Fixes #N) libkernel needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libkernel: sigprocmask shares masks between threads and loses blocked signal delivery on Windows

6 participants