In looking into this sigprocmask business some more, I discovered a
problem that can lead to subtle race condition bugs on macOS. Gnulib
assumes that in a multithreaded process sigprocmask behaves like
pthread_sigmask (aside from minor API changes) if both functions exist.
Although that's typically true, it's false on Darwin-based systems[1]
such as macOS.
This can lead to races in multithreaded macOS processes, e.g., one
thread uses sigprocmask while another one uses pthread_sigmask, and the
first one steps on the second one's signal mask. Races can also occur if
both threads use sigprocmask.
For years, POSIX has recommended that multithreaded apps use
pthread_sigmask instead of sigprocmask. In some sense that's the
cleanest way for Gnulib to go, too. So far Gnulib has often not followed
POSIX's recommendation, because pthread_sigmask required linking with
-lpthread on many platforms, but that hasn't been true for glibc since
glibc 2.32 (2020), and now sounds like a good time to stop worrying
about linking performance on older glibc (as well as on NetBSD, OpenBSD,
AIX).
So I propose altering Gnulib to do things as POSIX suggests, adjusting
linking instructions as needed. In the meantime I installed the attached
patch to document the issue, and to add FIXMEs where it seems that
sigprocmask should be replaced by pthread_sigmask.
In this patch I did not add a FIXME for sigpipe-die; instead, I added a
comment saying that install_sigpipe_die_handler should be called only in
a single-threaded process, as I expect switching sigpipe-die to using
pthread_sigmask would not change that.
[1]:
https://github.com/apple-oss-distributions/xnu/blob/f6217f891ac0bb64f3d375211650a4c1ff8ca1ea/bsd/kern/kern_sig.c#L592From 2009f8fd338429084781cca66d2e32761260aea3 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Fri, 3 Apr 2026 19:55:48 -0700
Subject: [PATCH] sigprocmask: document race bugs on macOS
In documentation and comments, mention macOS sigprocmask
incompatibility with GNU and most other systems, and note Gnulib
uses of sigprocmask that can cause subtle race condition bugs on
macOS. Although an obvious fix is to switch to pthread_sigmask as
POSIX suggests, that will require some changing to linking
instructions, and the first step is documentation.
---
ChangeLog | 8 +++++++
doc/posix-functions/pthread_sigmask.texi | 2 +-
doc/posix-functions/sigprocmask.texi | 28 +++++++++++++++--------
lib/asyncsafe-spin.c | 11 +++++++--
lib/execute.c | 3 +++
lib/fatal-signal.c | 6 +++++
lib/sigpipe-die.h | 3 ++-
lib/spawn-pipe.c | 3 +++
lib/spawni.c | 3 +++
lib/term-style-control.c | 6 +++++
tests/test-asyncsafe-linked_list-strong.c | 5 +++-
tests/test-asyncsafe-linked_list-weak.c | 5 +++-
12 files changed, 68 insertions(+), 15 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index 48ac41eee4..3b44833968 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
2026-04-03 Paul Eggert <[email protected]>
+ sigprocmask: document race bugs on macOS
+ In documentation and comments, mention macOS sigprocmask
+ incompatibility with GNU and most other systems, and note Gnulib
+ uses of sigprocmask that can cause subtle race condition bugs on
+ macOS. Although an obvious fix is to switch to pthread_sigmask as
+ POSIX suggests, that will require some changing to linking
+ instructions, and the first step is documentation.
+
sigprocmask: single-thread optimization typo fix
* lib/sigprocmask.c (gl_lock_define_initialized):
Swap #if’s then- and else-parts.
diff --git a/doc/posix-functions/pthread_sigmask.texi b/doc/posix-functions/pthread_sigmask.texi
index 5149939c6c..74f3ab909b 100644
--- a/doc/posix-functions/pthread_sigmask.texi
+++ b/doc/posix-functions/pthread_sigmask.texi
@@ -21,7 +21,7 @@ This function does nothing and always returns 0 in programs that are not
linked with @code{-lpthread} on some platforms:
FreeBSD 14.0, MidnightBSD 1.1, HP-UX 11.31, Solaris 9.
@item
-When it fails, this functions returns @minus{}1 instead of the error number on
+When it fails, this function returns @minus{}1 instead of the error number on
some platforms:
Cygwin 1.7.5.
@end itemize
diff --git a/doc/posix-functions/sigprocmask.texi b/doc/posix-functions/sigprocmask.texi
index 02bc537614..e4f9469c6b 100644
--- a/doc/posix-functions/sigprocmask.texi
+++ b/doc/posix-functions/sigprocmask.texi
@@ -20,17 +20,27 @@ Portability problems not fixed by Gnulib:
In case of failure, the return value is wrong on some platforms:
@c https://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=57213
NetBSD 10.0 when libpthread is in use.
-@end itemize
-Note: Although @code{sigprocmask} officially has undefined behaviour in
-multi-threaded programs, in practice it is essentially equivalent to
+@item
+POSIX says that in multi-threaded programs @code{sigprocmask} has
+unspecified behavior so @code{pthread_sigmask} should be used instead.
+On most platforms, @code{sigprocmask} is essentially equivalent to
@code{pthread_sigmask}, with only a difference regarding the error
-return convention. It's simpler to use @code{sigprocmask}, since it does
-not require linking with @code{-lpthread} on some platforms:
-glibc, NetBSD, OpenBSD, AIX.
+return convention.
+However, on Darwin-based systems such as macOS, @code{sigprocmask} can
+affect the signal masks of other threads, and this discrepancy can
+lead to race conditions, e.g., if some threads use @code{sigprocmask}
+while others use @code{pthread_sigmask}.
+@end itemize
+
+Note: In single-threaded applications it's simpler to use
+@code{sigprocmask}, since it does not require compiling with
+@code{-pthread} and/or linking with @code{-lpthread} on some platforms:
+glibc 2.31, NetBSD, OpenBSD, AIX.
Note: While on POSIX platforms, @code{sigprocmask} is multithread-safe
-and async-signal safe (cf. POSIX section ``Signal Actions''
-@url{https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03}),
+and async-signal-safe (see
+@url{https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03,
+POSIX section ``Signal Actions''}),
the gnulib replacement on native Windows is only multithread-safe,
-not async-signal safe.
+not async-signal-safe.
diff --git a/lib/asyncsafe-spin.c b/lib/asyncsafe-spin.c
index 8e59da00ad..8400ad87a3 100644
--- a/lib/asyncsafe-spin.c
+++ b/lib/asyncsafe-spin.c
@@ -50,10 +50,14 @@ asyncsafe_spin_lock (asyncsafe_spinlock_t *lock,
Whereas on native Windows, sigprocmask() is not atomic, because it
manipulates global variables. Therefore in this case, we are *not*
allowed to call it from within a signal handler. */
+
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
#if defined _WIN32 && !defined __CYGWIN__
if (!from_signal_handler)
#endif
- sigprocmask (SIG_BLOCK, mask, saved_mask); /* equivalent to pthread_sigmask */
+ sigprocmask (SIG_BLOCK, mask, saved_mask);
glthread_spinlock_lock (lock);
}
@@ -66,10 +70,13 @@ asyncsafe_spin_unlock (asyncsafe_spinlock_t *lock,
if (glthread_spinlock_unlock (lock))
abort ();
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
#if defined _WIN32 && !defined __CYGWIN__
if (!from_signal_handler)
#endif
- sigprocmask (SIG_SETMASK, saved_mask, NULL); /* equivalent to pthread_sigmask */
+ sigprocmask (SIG_SETMASK, saved_mask, NULL);
}
void
diff --git a/lib/execute.c b/lib/execute.c
index 17b6af05a8..a6cc6c6bc7 100644
--- a/lib/execute.c
+++ b/lib/execute.c
@@ -283,6 +283,9 @@ execute (const char *progname,
if (slave_process)
{
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_SETMASK, NULL, &blocked_signals);
block_fatal_signals ();
}
diff --git a/lib/fatal-signal.c b/lib/fatal-signal.c
index e790731534..c4a8a36403 100644
--- a/lib/fatal-signal.c
+++ b/lib/fatal-signal.c
@@ -311,6 +311,9 @@ block_fatal_signals (void)
if (fatal_signals_block_counter++ == 0)
{
init_fatal_signal_set ();
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_BLOCK, &fatal_signal_set, NULL);
}
@@ -332,6 +335,9 @@ unblock_fatal_signals (void)
if (--fatal_signals_block_counter == 0)
{
init_fatal_signal_set ();
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_UNBLOCK, &fatal_signal_set, NULL);
}
diff --git a/lib/sigpipe-die.h b/lib/sigpipe-die.h
index 076434e417..6cf56c6ec3 100644
--- a/lib/sigpipe-die.h
+++ b/lib/sigpipe-die.h
@@ -56,7 +56,8 @@ extern "C" {
/*extern*/ _Noreturn void sigpipe_die (void);
/* Install a SIGPIPE handler that invokes PREPARE_DIE and then emits an
- error message and exits. PREPARE_DIE may be NULL, meaning a no-op. */
+ error message and exits. PREPARE_DIE may be NULL, meaning a no-op.
+ This function should not be called in a multithreaded process. */
extern void install_sigpipe_die_handler (void (*prepare_die) (void));
diff --git a/lib/spawn-pipe.c b/lib/spawn-pipe.c
index b76f5c91d9..ea03586ce7 100644
--- a/lib/spawn-pipe.c
+++ b/lib/spawn-pipe.c
@@ -453,6 +453,9 @@ create_pipe (const char *progname,
sigset_t blocked_signals;
if (slave_process)
{
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_SETMASK, NULL, &blocked_signals);
block_fatal_signals ();
}
diff --git a/lib/spawni.c b/lib/spawni.c
index 0326d89a3e..4c6d54e01c 100644
--- a/lib/spawni.c
+++ b/lib/spawni.c
@@ -937,6 +937,9 @@ __spawni (pid_t *pid, const char *file,
}
/* Set signal mask. */
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
if ((flags & POSIX_SPAWN_SETSIGMASK) != 0
&& sigprocmask (SIG_SETMASK, &attrp->_ss, NULL) != 0)
_exit (SPAWN_ERROR);
diff --git a/lib/term-style-control.c b/lib/term-style-control.c
index 5356a4d9c5..b2449964a9 100644
--- a/lib/term-style-control.c
+++ b/lib/term-style-control.c
@@ -575,6 +575,9 @@ block_relevant_signals ()
if (!relevant_signal_set_initialized)
abort ();
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_BLOCK, &relevant_signal_set, NULL);
}
@@ -582,6 +585,9 @@ block_relevant_signals ()
static _GL_ASYNC_SAFE inline void
unblock_relevant_signals ()
{
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
sigprocmask (SIG_UNBLOCK, &relevant_signal_set, NULL);
}
diff --git a/tests/test-asyncsafe-linked_list-strong.c b/tests/test-asyncsafe-linked_list-strong.c
index 4a3e4c884c..253c4c4cac 100644
--- a/tests/test-asyncsafe-linked_list-strong.c
+++ b/tests/test-asyncsafe-linked_list-strong.c
@@ -124,7 +124,10 @@ block_sigint (void)
sigemptyset (&mask);
sigaddset (&mask, MY_SIGNAL);
- sigprocmask (SIG_BLOCK, &mask, NULL); /* equivalent to pthread_sigmask */
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
+ sigprocmask (SIG_BLOCK, &mask, NULL);
}
/* This thread is idle. */
diff --git a/tests/test-asyncsafe-linked_list-weak.c b/tests/test-asyncsafe-linked_list-weak.c
index 4f1e1c5025..fc1f3b35b2 100644
--- a/tests/test-asyncsafe-linked_list-weak.c
+++ b/tests/test-asyncsafe-linked_list-weak.c
@@ -241,7 +241,10 @@ block_sigint (void)
sigemptyset (&mask);
sigaddset (&mask, MY_SIGNAL);
- sigprocmask (SIG_BLOCK, &mask, NULL); /* equivalent to pthread_sigmask */
+ /* FIXME: Use pthread_sigmask, not sigprocmask, as the two functions
+ behave differently on macOS and the sigprocmask behavior can cause
+ this thread to race with other threads in harmful ways. */
+ sigprocmask (SIG_BLOCK, &mask, NULL);
}
/* This thread is idle. */
--
2.51.0