Commit 9cc537c5 authored by Kenton Varda's avatar Kenton Varda

Work around broken `sigprocmask()` and `siglongjmp()` on macOS.

Apparently, `sigprocmask()` on macOS affects all threads. You must use `pthread_sigmask()` to affect only the current thread. (To be fair, POSIX says that `sigprocmask()` has unspecified behavior in the presence of threads. However, having it affect all threads is bizarre. On Linux, it affects only the calling thread.)

Moreover, `siglongjmp()` on macOS is implemented in terms of `sigprocmask()`. Hence, `siglongjmp()` (with `true` for the second parameter) is not safe to use in a multithreaded program.

Instead, we must decompose `siglongjmp()` into its component parts, so that we can correctly use `pthread_sigmask()`.

This fixes the deadlocks on macOS.
parent e54850e9
...@@ -742,7 +742,7 @@ struct TestChild { ...@@ -742,7 +742,7 @@ struct TestChild {
sigset_t sigs; sigset_t sigs;
sigemptyset(&sigs); sigemptyset(&sigs);
sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGTERM);
sigprocmask(SIG_UNBLOCK, &sigs, nullptr); pthread_sigmask(SIG_UNBLOCK, &sigs, nullptr);
for (;;) pause(); for (;;) pause();
} }
...@@ -775,8 +775,8 @@ TEST(AsyncUnixTest, ChildProcess) { ...@@ -775,8 +775,8 @@ TEST(AsyncUnixTest, ChildProcess) {
sigset_t sigs, oldsigs; sigset_t sigs, oldsigs;
KJ_SYSCALL(sigemptyset(&sigs)); KJ_SYSCALL(sigemptyset(&sigs));
KJ_SYSCALL(sigaddset(&sigs, SIGTERM)); KJ_SYSCALL(sigaddset(&sigs, SIGTERM));
KJ_SYSCALL(sigprocmask(SIG_BLOCK, &sigs, &oldsigs)); KJ_SYSCALL(pthread_sigmask(SIG_BLOCK, &sigs, &oldsigs));
KJ_DEFER(KJ_SYSCALL(sigprocmask(SIG_SETMASK, &oldsigs, nullptr)) { break; }); KJ_DEFER(KJ_SYSCALL(pthread_sigmask(SIG_SETMASK, &oldsigs, nullptr)) { break; });
TestChild child1(port, 123); TestChild child1(port, 123);
KJ_EXPECT(!child1.promise.poll(waitScope)); KJ_EXPECT(!child1.promise.poll(waitScope));
......
...@@ -56,6 +56,19 @@ bool threadClaimedChildExits = false; ...@@ -56,6 +56,19 @@ bool threadClaimedChildExits = false;
struct SignalCapture { struct SignalCapture {
sigjmp_buf jumpTo; sigjmp_buf jumpTo;
siginfo_t siginfo; siginfo_t siginfo;
sigset_t originalMask;
// The signal mask to be restored when jumping out of the signal handler.
//
// "But wait!" you say, "Isn't the whole point of siglongjmp() that it does this for you?" Well,
// yes, that is supposed to be the point. However, Apple implemented in wrong. On macOS,
// siglongjmp() uses sigprocmask() -- not pthread_sigmask() -- to restore the signal mask.
// Unfortunately, sigprocmask() on macOS affects threads other than the current thread. Arguably
// this is conformant: sigprocmask() is documented as having unspecified behavior in the presence
// of threads, and pthread_sigmask() must be used instead. However, this means siglongjmp()
// cannot be used in the presence of threads.
//
// We'll just have to restore the signal mask ourselves, rather than rely on siglongjmp()...
}; };
#if !KJ_USE_EPOLL // on Linux we'll use signalfd #if !KJ_USE_EPOLL // on Linux we'll use signalfd
...@@ -65,7 +78,14 @@ void signalHandler(int, siginfo_t* siginfo, void*) { ...@@ -65,7 +78,14 @@ void signalHandler(int, siginfo_t* siginfo, void*) {
SignalCapture* capture = threadCapture; SignalCapture* capture = threadCapture;
if (capture != nullptr) { if (capture != nullptr) {
capture->siginfo = *siginfo; capture->siginfo = *siginfo;
siglongjmp(capture->jumpTo, 1);
// See comments on SignalCapture::originalMask, above: We can't rely on siglongjmp() to restore
// the signal mask; we must do it ourselves using pthread_sigmask(). We pass false as the
// second parameter to siglongjmp() so that it skips changing the signal mask. This makes it
// equivalent to `longjmp()` on Linux or `_longjmp()` on BSD/macOS. See comments on
// SignalCapture::originalMask for explanation.
pthread_sigmask(SIG_SETMASK, &capture->originalMask, nullptr);
siglongjmp(capture->jumpTo, false);
} }
} }
#endif #endif
...@@ -76,7 +96,7 @@ void registerSignalHandler(int signum) { ...@@ -76,7 +96,7 @@ void registerSignalHandler(int signum) {
sigset_t mask; sigset_t mask;
KJ_SYSCALL(sigemptyset(&mask)); KJ_SYSCALL(sigemptyset(&mask));
KJ_SYSCALL(sigaddset(&mask, signum)); KJ_SYSCALL(sigaddset(&mask, signum));
KJ_SYSCALL(sigprocmask(SIG_BLOCK, &mask, nullptr)); KJ_SYSCALL(pthread_sigmask(SIG_BLOCK, &mask, nullptr));
#if !KJ_USE_EPOLL // on Linux we'll use signalfd #if !KJ_USE_EPOLL // on Linux we'll use signalfd
struct sigaction action; struct sigaction action;
...@@ -839,7 +859,7 @@ bool UnixEventPort::wait() { ...@@ -839,7 +859,7 @@ bool UnixEventPort::wait() {
// Capture signals. // Capture signals.
SignalCapture capture; SignalCapture capture;
if (sigsetjmp(capture.jumpTo, true)) { if (sigsetjmp(capture.jumpTo, false)) {
// We received a signal and longjmp'd back out of the signal handler. // We received a signal and longjmp'd back out of the signal handler.
threadCapture = nullptr; threadCapture = nullptr;
...@@ -852,16 +872,15 @@ bool UnixEventPort::wait() { ...@@ -852,16 +872,15 @@ bool UnixEventPort::wait() {
} }
// Enable signals, run the poll, then mask them again. // Enable signals, run the poll, then mask them again.
sigset_t origMask;
threadCapture = &capture; threadCapture = &capture;
sigprocmask(SIG_UNBLOCK, &newMask, &origMask); pthread_sigmask(SIG_UNBLOCK, &newMask, &capture.originalMask);
pollContext.run( pollContext.run(
timerImpl.timeoutToNextEvent(clock.now(), MILLISECONDS, int(maxValue)) timerImpl.timeoutToNextEvent(clock.now(), MILLISECONDS, int(maxValue))
.map([](uint64_t t) -> int { return t; }) .map([](uint64_t t) -> int { return t; })
.orDefault(-1)); .orDefault(-1));
sigprocmask(SIG_SETMASK, &origMask, nullptr); pthread_sigmask(SIG_SETMASK, &capture.originalMask, nullptr);
threadCapture = nullptr; threadCapture = nullptr;
// Queue events. // Queue events.
...@@ -904,23 +923,26 @@ bool UnixEventPort::poll() { ...@@ -904,23 +923,26 @@ bool UnixEventPort::poll() {
// Wait for each pending signal. It would be nice to use sigtimedwait() here but it is not // Wait for each pending signal. It would be nice to use sigtimedwait() here but it is not
// available on OSX. :( Instead, we call sigsuspend() once per expected signal. // available on OSX. :( Instead, we call sigsuspend() once per expected signal.
while (signalCount-- > 0) { {
SignalCapture capture; SignalCapture capture;
pthread_sigmask(SIG_SETMASK, nullptr, &capture.originalMask);
threadCapture = &capture; threadCapture = &capture;
if (sigsetjmp(capture.jumpTo, true)) { KJ_DEFER(threadCapture = nullptr);
// We received a signal and longjmp'd back out of the signal handler. while (signalCount-- > 0) {
sigdelset(&waitMask, capture.siginfo.si_signo); if (sigsetjmp(capture.jumpTo, false)) {
if (capture.siginfo.si_signo == reservedSignal) { // We received a signal and longjmp'd back out of the signal handler.
woken = true; sigdelset(&waitMask, capture.siginfo.si_signo);
if (capture.siginfo.si_signo == reservedSignal) {
woken = true;
} else {
gotSignal(capture.siginfo);
}
} else { } else {
gotSignal(capture.siginfo); sigsuspend(&waitMask);
KJ_FAIL_ASSERT("sigsuspend() shouldn't return because the signal handler should "
"have siglongjmp()ed.");
} }
} else {
sigsuspend(&waitMask);
KJ_FAIL_ASSERT("sigsuspend() shouldn't return because the signal handler should "
"have siglongjmp()ed.");
} }
threadCapture = nullptr;
} }
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment