git: 16a284b1cdfd - main - linux(4): Fix signal mask restoration in epoll_pwait(2)/epoll_pwait2(2)

From: Konstantin Belousov <kib_at_FreeBSD.org>
Date: Sun, 27 Sep 2026 19:26:35 UTC
The branch main has been updated by kib:

URL: https://cgit.FreeBSD.org/src/commit/?id=16a284b1cdfd45ba99c2723e7f88497e76b235ad

commit 16a284b1cdfd45ba99c2723e7f88497e76b235ad
Author:     Grzegorz Junka <list1@gjunka.com>
AuthorDate: 2026-09-27 19:22:20 +0000
Commit:     Konstantin Belousov <kib@FreeBSD.org>
CommitDate: 2026-09-27 19:22:20 +0000

    linux(4): Fix signal mask restoration in epoll_pwait(2)/epoll_pwait2(2)
    
    Since 80c7315d17ce ("Restore signal mask in epoll_pwait.") the caller's
    signal mask is saved in a local variable, but TDP_OLDMASK is still set
    and the TDA_SIGSUSPEND AST is still scheduled.  On return to user mode
    that AST, or postsig() if a signal is delivered, then installs
    td_oldsigmask, which this code never writes and which holds whatever
    mask the thread had at its last sigsuspend(2), pselect(2) or ppoll(2).
    As a result every epoll_pwait(2)/epoll_pwait2(2) call with a non-NULL
    sigmask can leave the thread with a stale signal mask.  In addition, the
    explicit restore at the end overwrote the return value, so EINTR (and
    any other error) was reported to user space as 0.
    
    Save the old mask in td_oldsigmask and let the AST restore it, as
    kern_pselect() and kern_poll_kfds() do: schedule TDA_SIGSUSPEND if the
    wait was interrupted, so the signal is delivered with the temporary mask
    in place, and TDA_PSELECT otherwise.  This matches Linux, which restores
    the saved mask unless the syscall returns -EINTR.
    
    This deadlocks Bun-based programs such as Claude Code (>= 2.1.269) and
    opencode.  JavaScriptCore suspends threads for conservative GC stack
    scanning by sending SIGPWR and waiting for the target's handler, which
    calls sigsuspend(2) with SIGPWR blocked.  Afterwards td_oldsigmask
    contains SIGPWR, the event loop's next epoll_pwait2(2) (Bun always
    passes an empty sigmask) blocks SIGPWR on the main thread, and the next
    GC suspend request waits forever.
    
    PR:             298878
    Fixes:          80c7315d17ce ("Restore signal mask in epoll_pwait.")
    MFC after:      1 week
---
 sys/compat/linux/linux_event.c | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/sys/compat/linux/linux_event.c b/sys/compat/linux/linux_event.c
index fc3ef7c3e90a..de313aefff97 100644
--- a/sys/compat/linux/linux_event.c
+++ b/sys/compat/linux/linux_event.c
@@ -376,7 +376,6 @@ linux_epoll_wait_ts(struct thread *td, int epfd, struct epoll_event *events,
 					NULL};
 	cap_rights_t rights;
 	struct file *epfp;
-	sigset_t omask;
 	int error;
 
 	if (maxevents <= 0 || maxevents > LINUX_MAX_EVENTS)
@@ -392,16 +391,10 @@ linux_epoll_wait_ts(struct thread *td, int epfd, struct epoll_event *events,
 	}
 	if (uset != NULL) {
 		error = kern_sigprocmask(td, SIG_SETMASK, uset,
-		    &omask, 0);
+		    &td->td_oldsigmask, 0);
 		if (error != 0)
 			goto leave;
 		td->td_pflags |= TDP_OLDMASK;
-		/*
-		 * Make sure that ast() is called on return to
-		 * usermode and TDP_OLDMASK is cleared, restoring old
-		 * sigmask.
-		 */
-		ast_sched(td, TDA_SIGSUSPEND);
 	}
 
 	coargs.leventlist = events;
@@ -420,9 +413,19 @@ linux_epoll_wait_ts(struct thread *td, int epfd, struct epoll_event *events,
 	if (error == 0)
 		td->td_retval[0] = coargs.count;
 
-	if (uset != NULL)
-		error = kern_sigprocmask(td, SIG_SETMASK, &omask,
-		    NULL, 0);
+	if (uset != NULL) {
+		/*
+		 * Make sure that ast() is called on return to usermode and
+		 * TDP_OLDMASK is cleared, restoring the old sigmask from
+		 * td_oldsigmask.  As on Linux, if we were interrupted, deliver
+		 * the signal with the temporary mask in place; otherwise
+		 * restore the old mask before any signal can be delivered.
+		 */
+		if (error == EINTR)
+			ast_sched(td, TDA_SIGSUSPEND);
+		else
+			ast_sched(td, TDA_PSELECT);
+	}
 leave:
 	fdrop(epfp, td);
 	return (error);