For several years, I'm seeing occasional failures of the 'test-asyncsafe-spin2'
test on native Windows machines with 2 or more CPUs. These failures are caused
by the sigprocmask replacement in gnulib, that is not multithread-safe.

The first patch fixes that.

Then, there is a second race condition on native Windows (not exhibited by a
unit test): If a sigprocmask() invocation happens to be interrupted by a signal,
and the signal handler invokes sigprocmask(), things may crash because
the sigprocmask replacement in gnulib is not async-signal safe.

The second patch fixes that by not invoking sigprocmask() from a signal handler.


2025-05-16  Bruno Haible  <br...@clisp.org>

        asyncsafe-spin: Fix race condition on native Windows.
        * lib/asyncsafe-spin.h (asyncsafe_spin_lock, asyncsafe_spin_unlock): Add
        from_signal_handler parameter.
        * lib/asyncsafe-spin.c (asyncsafe_spin_lock, asyncsafe_spin_unlock):
        Likewise.
        * modules/asyncsafe-spin (Depends-on): Add bool.
        * tests/test-asyncsafe-spin1.c (main): Update.
        * tests/test-asyncsafe-spin2.c (lock_mutator_thread,
        lock_checker_thread): Update.
        * lib/clean-temp-simple.c (clean_temp_asyncsafe_close): Update.
        * lib/clean-temp.c (asyncsafe_fclose_variant): Update.

2025-05-16  Bruno Haible  <br...@clisp.org>

        sigprocmask: Make multithread-safe on native Windows.
        * lib/sigprocmask.c: Include glthread/lock.h.
        (sig_lock): New variable.
        (blocked_set): Remove 'volatile'.
        (sigprocmask, _gl_raise_SIGPIPE): Use the sig_lock.
        * modules/sigprocmask (Depends-on): Add lock.
        * doc/posix-functions/sigprocmask.texi: Mention the async-safety issue.

>From e9d8c4f480caf32ef072d85d26af68bc9a68d2fb Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Fri, 16 May 2025 15:51:13 +0200
Subject: [PATCH 1/2] sigprocmask: Make multithread-safe on native Windows.

* lib/sigprocmask.c: Include glthread/lock.h.
(sig_lock): New variable.
(blocked_set): Remove 'volatile'.
(sigprocmask, _gl_raise_SIGPIPE): Use the sig_lock.
* modules/sigprocmask (Depends-on): Add lock.
* doc/posix-functions/sigprocmask.texi: Mention the async-safety issue.
---
 ChangeLog                            | 10 ++++++++++
 doc/posix-functions/sigprocmask.texi |  6 ++++++
 lib/sigprocmask.c                    | 20 ++++++++++++++++++--
 modules/sigprocmask                  |  1 +
 4 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 95323c3a19..25cced87c6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2025-05-16  Bruno Haible  <br...@clisp.org>
+
+	sigprocmask: Make multithread-safe on native Windows.
+	* lib/sigprocmask.c: Include glthread/lock.h.
+	(sig_lock): New variable.
+	(blocked_set): Remove 'volatile'.
+	(sigprocmask, _gl_raise_SIGPIPE): Use the sig_lock.
+	* modules/sigprocmask (Depends-on): Add lock.
+	* doc/posix-functions/sigprocmask.texi: Mention the async-safety issue.
+
 2025-05-16  Bruno Haible  <br...@clisp.org>
 
 	pthread-rwlock tests: Add a comment.
diff --git a/doc/posix-functions/sigprocmask.texi b/doc/posix-functions/sigprocmask.texi
index 7683986b3f..02bc537614 100644
--- a/doc/posix-functions/sigprocmask.texi
+++ b/doc/posix-functions/sigprocmask.texi
@@ -28,3 +28,9 @@
 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.
+
+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}),
+the gnulib replacement on native Windows is only multithread-safe,
+not async-signal safe.
diff --git a/lib/sigprocmask.c b/lib/sigprocmask.c
index f79386c8d0..d52e191c44 100644
--- a/lib/sigprocmask.c
+++ b/lib/sigprocmask.c
@@ -24,6 +24,8 @@
 #include <stdint.h>
 #include <stdlib.h>
 
+#include "glthread/lock.h"
+
 #if HAVE_MSVC_INVALID_PARAMETER_HANDLER
 # include "msvc-inval.h"
 #endif
@@ -84,6 +86,10 @@ signal_nothrow (int sig, handler_t handler)
 # define signal signal_nothrow
 #endif
 
+/* This lock protects the variables defined in this file from concurrent
+   modification in multiple threads.  */
+gl_lock_define_initialized(static, sig_lock)
+
 /* Handling of gnulib defined signals.  */
 
 #if GNULIB_defined_SIGPIPE
@@ -182,7 +188,7 @@ sigfillset (sigset_t *set)
 }
 
 /* Set of currently blocked signals.  */
-static volatile sigset_t blocked_set /* = 0 */;
+static sigset_t blocked_set /* = 0 */;
 
 /* Set of currently blocked and pending signals.  */
 static volatile sig_atomic_t pending_array[NSIG] /* = { 0 } */;
@@ -221,6 +227,8 @@ static volatile handler_t old_handlers[NSIG];
 int
 sigprocmask (int operation, const sigset_t *set, sigset_t *old_set)
 {
+  gl_lock_lock (sig_lock);
+
   if (old_set != NULL)
     *old_set = blocked_set;
 
@@ -242,6 +250,7 @@ sigprocmask (int operation, const sigset_t *set, sigset_t *old_set)
           new_blocked_set = blocked_set & ~*set;
           break;
         default:
+          gl_lock_unlock (sig_lock);
           errno = EINVAL;
           return -1;
         }
@@ -286,6 +295,8 @@ sigprocmask (int operation, const sigset_t *set, sigset_t *old_set)
               raise (sig);
         }
     }
+
+  gl_lock_unlock (sig_lock);
   return 0;
 }
 
@@ -334,11 +345,16 @@ rpl_signal (int sig, handler_t handler)
 int
 _gl_raise_SIGPIPE (void)
 {
+  gl_lock_lock (sig_lock);
   if (blocked_set & (1U << SIGPIPE))
-    pending_array[SIGPIPE] = 1;
+    {
+      pending_array[SIGPIPE] = 1;
+      gl_lock_unlock (sig_lock);
+    }
   else
     {
       handler_t handler = SIGPIPE_handler;
+      gl_lock_unlock (sig_lock);
       if (handler == SIG_DFL)
         exit (128 + SIGPIPE);
       else if (handler != SIG_IGN)
diff --git a/modules/sigprocmask b/modules/sigprocmask
index 7f2e083803..79e7c7302f 100644
--- a/modules/sigprocmask
+++ b/modules/sigprocmask
@@ -8,6 +8,7 @@ m4/signalblocking.m4
 Depends-on:
 signal-h
 stdint-h        [test $HAVE_POSIX_SIGNALBLOCKING = 0]
+lock            [test $HAVE_POSIX_SIGNALBLOCKING = 0]
 raise           [test $HAVE_POSIX_SIGNALBLOCKING = 0]
 msvc-inval      [test $HAVE_POSIX_SIGNALBLOCKING = 0]
 
-- 
2.43.0

>From 9ea3eecee3eafc7cdb08d6167b387374d6dc1378 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Fri, 16 May 2025 16:37:03 +0200
Subject: [PATCH 2/2] asyncsafe-spin: Fix race condition on native Windows.

* lib/asyncsafe-spin.h (asyncsafe_spin_lock, asyncsafe_spin_unlock): Add
from_signal_handler parameter.
* lib/asyncsafe-spin.c (asyncsafe_spin_lock, asyncsafe_spin_unlock):
Likewise.
* modules/asyncsafe-spin (Depends-on): Add bool.
* tests/test-asyncsafe-spin1.c (main): Update.
* tests/test-asyncsafe-spin2.c (lock_mutator_thread,
lock_checker_thread): Update.
* lib/clean-temp-simple.c (clean_temp_asyncsafe_close): Update.
* lib/clean-temp.c (asyncsafe_fclose_variant): Update.
---
 ChangeLog                    | 14 ++++++++++++++
 lib/asyncsafe-spin.c         | 33 ++++++++++++++++++++++++++++++---
 lib/asyncsafe-spin.h         |  6 ++++--
 lib/clean-temp-simple.c      |  4 ++--
 lib/clean-temp.c             |  6 ++++--
 modules/asyncsafe-spin       |  1 +
 tests/test-asyncsafe-spin1.c |  8 ++++----
 tests/test-asyncsafe-spin2.c | 20 ++++++++++----------
 8 files changed, 69 insertions(+), 23 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 25cced87c6..e628b7169f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,17 @@
+2025-05-16  Bruno Haible  <br...@clisp.org>
+
+	asyncsafe-spin: Fix race condition on native Windows.
+	* lib/asyncsafe-spin.h (asyncsafe_spin_lock, asyncsafe_spin_unlock): Add
+	from_signal_handler parameter.
+	* lib/asyncsafe-spin.c (asyncsafe_spin_lock, asyncsafe_spin_unlock):
+	Likewise.
+	* modules/asyncsafe-spin (Depends-on): Add bool.
+	* tests/test-asyncsafe-spin1.c (main): Update.
+	* tests/test-asyncsafe-spin2.c (lock_mutator_thread,
+	lock_checker_thread): Update.
+	* lib/clean-temp-simple.c (clean_temp_asyncsafe_close): Update.
+	* lib/clean-temp.c (asyncsafe_fclose_variant): Update.
+
 2025-05-16  Bruno Haible  <br...@clisp.org>
 
 	sigprocmask: Make multithread-safe on native Windows.
diff --git a/lib/asyncsafe-spin.c b/lib/asyncsafe-spin.c
index 5dbefa1c60..38740e817c 100644
--- a/lib/asyncsafe-spin.c
+++ b/lib/asyncsafe-spin.c
@@ -31,18 +31,45 @@ asyncsafe_spin_init (asyncsafe_spinlock_t *lock)
 
 void
 asyncsafe_spin_lock (asyncsafe_spinlock_t *lock,
+                     bool from_signal_handler,
                      const sigset_t *mask, sigset_t *saved_mask)
 {
-  sigprocmask (SIG_BLOCK, mask, saved_mask); /* equivalent to pthread_sigmask */
+  /* On all platforms, when not running in a signal handler, we need to block
+     the signals until the corresponding asyncsafe_spin_unlock() invocation.
+     This is needed because if, during that period, a signal occurred and it
+     happened to run in the current thread and it were to wait on this spin
+     lock, it would hang.
+     On platforms other than native Windows, it is useful to do the same
+     thing also within a signal handler, since signals may remain enabled
+     while a signal handler runs.  It is possible to do this because
+     sigprocmask() is safe to call from within a signal handler, see
+     POSIX section "Signal Actions"
+     <https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03>.
+     (In other words, sigprocmask() is atomic, because it is implemented as a
+     system call.)
+     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.  */
+#if defined _WIN32 && !defined __CYGWIN__
+  if (!from_signal_handler)
+#endif
+    sigprocmask (SIG_BLOCK, mask, saved_mask); /* equivalent to pthread_sigmask */
+
   glthread_spinlock_lock (lock);
 }
 
 void
-asyncsafe_spin_unlock (asyncsafe_spinlock_t *lock, const sigset_t *saved_mask)
+asyncsafe_spin_unlock (asyncsafe_spinlock_t *lock,
+                       bool from_signal_handler,
+                       const sigset_t *saved_mask)
 {
   if (glthread_spinlock_unlock (lock))
     abort ();
-  sigprocmask (SIG_SETMASK, saved_mask, NULL); /* equivalent to pthread_sigmask */
+
+#if defined _WIN32 && !defined __CYGWIN__
+  if (!from_signal_handler)
+#endif
+    sigprocmask (SIG_SETMASK, saved_mask, NULL); /* equivalent to pthread_sigmask */
 }
 
 void
diff --git a/lib/asyncsafe-spin.h b/lib/asyncsafe-spin.h
index 1bbf2f4702..c2a4b6bdfe 100644
--- a/lib/asyncsafe-spin.h
+++ b/lib/asyncsafe-spin.h
@@ -28,9 +28,9 @@
    both in regular multithreaded code and in signal handlers:
 
        sigset_t saved_mask;
-       asyncsafe_spin_lock (&lock, &mask, &saved_mask);
+       asyncsafe_spin_lock (&lock, from_signal_handler, &mask, &saved_mask);
        do_something_contentious ();
-       asyncsafe_spin_unlock (&lock, &saved_mask);
+       asyncsafe_spin_unlock (&lock, from_signal_handler, &saved_mask);
 
    The mask you specify here is the set of signals whose handlers might want to
    take the same lock.
@@ -53,8 +53,10 @@ extern "C" {
 
 extern void asyncsafe_spin_init (asyncsafe_spinlock_t *lock);
 extern void asyncsafe_spin_lock (asyncsafe_spinlock_t *lock,
+                                 bool from_signal_handler,
                                  const sigset_t *mask, sigset_t *saved_mask);
 extern void asyncsafe_spin_unlock (asyncsafe_spinlock_t *lock,
+                                   bool from_signal_handler,
                                    const sigset_t *saved_mask);
 extern void asyncsafe_spin_destroy (asyncsafe_spinlock_t *lock);
 
diff --git a/lib/clean-temp-simple.c b/lib/clean-temp-simple.c
index 727ff559f2..95103b8ac0 100644
--- a/lib/clean-temp-simple.c
+++ b/lib/clean-temp-simple.c
@@ -130,7 +130,7 @@ clean_temp_asyncsafe_close (struct closeable_fd *element)
   int ret;
   int saved_errno;
 
-  asyncsafe_spin_lock (&element->lock, fatal_signal_set, &saved_mask);
+  asyncsafe_spin_lock (&element->lock, true, fatal_signal_set, &saved_mask);
   if (!element->closed)
     {
       ret = close (element->fd);
@@ -142,7 +142,7 @@ clean_temp_asyncsafe_close (struct closeable_fd *element)
       ret = 0;
       saved_errno = 0;
     }
-  asyncsafe_spin_unlock (&element->lock, &saved_mask);
+  asyncsafe_spin_unlock (&element->lock, true, &saved_mask);
   element->done = true;
 
   errno = saved_errno;
diff --git a/lib/clean-temp.c b/lib/clean-temp.c
index f477e7719e..e8d0cf3113 100644
--- a/lib/clean-temp.c
+++ b/lib/clean-temp.c
@@ -109,7 +109,8 @@ asyncsafe_fclose_variant (struct closeable_fd *element, FILE *fp,
   int ret;
   int saved_errno;
 
-  asyncsafe_spin_lock (&element->lock, get_fatal_signal_set (), &saved_mask);
+  asyncsafe_spin_lock (&element->lock, false,
+                       get_fatal_signal_set (), &saved_mask);
   if (!element->closed)
     {
       ret = fclose_variant (fp); /* invokes close (element->fd) */
@@ -121,7 +122,8 @@ asyncsafe_fclose_variant (struct closeable_fd *element, FILE *fp,
       ret = 0;
       saved_errno = 0;
     }
-  asyncsafe_spin_unlock (&element->lock, &saved_mask);
+  asyncsafe_spin_unlock (&element->lock, false,
+                         &saved_mask);
   element->done = true;
 
   errno = saved_errno;
diff --git a/modules/asyncsafe-spin b/modules/asyncsafe-spin
index d918096163..9c3d0097f1 100644
--- a/modules/asyncsafe-spin
+++ b/modules/asyncsafe-spin
@@ -7,6 +7,7 @@ lib/asyncsafe-spin.c
 
 Depends-on:
 signal-h
+bool
 sigprocmask
 spin
 
diff --git a/tests/test-asyncsafe-spin1.c b/tests/test-asyncsafe-spin1.c
index dd528f422e..9662bb255b 100644
--- a/tests/test-asyncsafe-spin1.c
+++ b/tests/test-asyncsafe-spin1.c
@@ -36,8 +36,8 @@ main (void)
   /* Check a spin-lock initialized through the constant initializer.  */
   {
     sigset_t saved_set;
-    asyncsafe_spin_lock (&global_spin_lock, &set, &saved_set);
-    asyncsafe_spin_unlock (&global_spin_lock, &saved_set);
+    asyncsafe_spin_lock (&global_spin_lock, false, &set, &saved_set);
+    asyncsafe_spin_unlock (&global_spin_lock, false, &saved_set);
   }
 
   /* Check a spin-lock initialized through asyncsafe_spin_init.  */
@@ -50,8 +50,8 @@ main (void)
     for (i = 0; i < 10; i++)
       {
         sigset_t saved_set;
-        asyncsafe_spin_lock (&local_spin_lock, &set, &saved_set);
-        asyncsafe_spin_unlock (&local_spin_lock, &saved_set);
+        asyncsafe_spin_lock (&local_spin_lock, false, &set, &saved_set);
+        asyncsafe_spin_unlock (&local_spin_lock, false, &saved_set);
       }
 
     asyncsafe_spin_destroy (&local_spin_lock);
diff --git a/tests/test-asyncsafe-spin2.c b/tests/test-asyncsafe-spin2.c
index 106aca24b9..b6e0563797 100644
--- a/tests/test-asyncsafe-spin2.c
+++ b/tests/test-asyncsafe-spin2.c
@@ -56,10 +56,10 @@
 #include "asyncsafe-spin.h"
 #if !ENABLE_LOCKING
 # define asyncsafe_spin_init(lock) (void)(lock)
-# define asyncsafe_spin_lock(lock, mask, saved_mask) \
-    ((void)(lock), (void)(mask), (void)(saved_mask))
-# define asyncsafe_spin_unlock(lock, saved_mask) \
-    ((void)(lock), (void)(saved_mask))
+# define asyncsafe_spin_lock(lock, from, mask, saved_mask) \
+    ((void)(lock), (void)(from), (void)(mask), (void)(saved_mask))
+# define asyncsafe_spin_unlock(lock, from, saved_mask) \
+    ((void)(lock), (void)(from), (void)(saved_mask))
 # define asyncsafe_spin_destroy(lock) (void)(lock)
 #endif
 
@@ -130,7 +130,7 @@ lock_mutator_thread (void *arg)
       int i1, i2, value;
 
       dbgprintf ("Mutator %p before lock\n", gl_thread_self_pointer ());
-      asyncsafe_spin_lock (&my_lock, &signals_to_block, &saved_signals);
+      asyncsafe_spin_lock (&my_lock, false, &signals_to_block, &saved_signals);
       dbgprintf ("Mutator %p after  lock\n", gl_thread_self_pointer ());
 
       i1 = random_account ();
@@ -140,13 +140,13 @@ lock_mutator_thread (void *arg)
       account[i2] -= value;
 
       dbgprintf ("Mutator %p before unlock\n", gl_thread_self_pointer ());
-      asyncsafe_spin_unlock (&my_lock, &saved_signals);
+      asyncsafe_spin_unlock (&my_lock, false, &saved_signals);
       dbgprintf ("Mutator %p after  unlock\n", gl_thread_self_pointer ());
 
       dbgprintf ("Mutator %p before check lock\n", gl_thread_self_pointer ());
-      asyncsafe_spin_lock (&my_lock, &signals_to_block, &saved_signals);
+      asyncsafe_spin_lock (&my_lock, false, &signals_to_block, &saved_signals);
       check_accounts ();
-      asyncsafe_spin_unlock (&my_lock, &saved_signals);
+      asyncsafe_spin_unlock (&my_lock, false, &saved_signals);
       dbgprintf ("Mutator %p after  check unlock\n", gl_thread_self_pointer ());
 
       yield ();
@@ -166,9 +166,9 @@ lock_checker_thread (void *arg)
       sigset_t saved_signals;
 
       dbgprintf ("Checker %p before check lock\n", gl_thread_self_pointer ());
-      asyncsafe_spin_lock (&my_lock, &signals_to_block, &saved_signals);
+      asyncsafe_spin_lock (&my_lock, false, &signals_to_block, &saved_signals);
       check_accounts ();
-      asyncsafe_spin_unlock (&my_lock, &saved_signals);
+      asyncsafe_spin_unlock (&my_lock, false, &saved_signals);
       dbgprintf ("Checker %p after  check unlock\n", gl_thread_self_pointer ());
 
       yield ();
-- 
2.43.0

Reply via email to