Paul Eggert wrote:
> When fixing this in gzip I noticed that the locking code in Gnulib's
> sigprocmask module is no longer necessary and in some sense is now
> misleading, now that Gnulib avoids callng sigprocmask from multithreaded
> processes and documents that sigprocmask is limited in that way. So I
> moved the locking to the pthread_sigmask module where POSIX suggests it
> belogs, and renamed the identifier to
> GNULIB_PTHREAD_SIGMASK_SINGLE_THREAD. Since no GNU apps use the old
> identifier this should be safe.
This looks right at first glance, but maybe a NEWS entry is warranted?
I'm saying this because the 'sigprocmask' module is used by the packages
diffutils, gettext, octave, wget, and others.
> Some of the sigpromask-related FIXMEs still remained in the fatal-signal
> and term-style-control modules, so I attempted to fix them too.
These two patches don't work, no. In my commits yesterday I mentioned that
those were the easy changes. fatal-signal and term-style-control are the
two hard cases, where pthread_sigmask does *not* work in a multithreaded
process.
* fatal-signal: This module is used, for example, by 'clean-temp', whose
purpose is to clean up a temporary file when a catchable fatal signal
like SIGINT is received. To this effect, the code
1. blocks the signal SIGINT,
2. creates the temporary file,
3. registers the temporary file in the data structures that are
inspected when SIGINT is received,
4. unblocks the signal SIGINT.
In a single-threaded process, a SIGINT that comes in between steps 2 and 3
is delayed until after step 4. This ensures that the temporary file gets
deleted.
But in a multi-threaded process, a SIGINT that comes in between steps 2 and 3
is delivered through another thread and terminates the process, without
having deleted the temporary file.
The fix for this will be by blocking the signal SIGINT across all threads
of the process. I'm attaching a draft module for this purpose, that I'm
working on.
* term-style-control: When I modify the unit test 'test-term-style-control-yes'
to install a second thread (that does nothing but nanosleep()), I see that
test program dump core sometimes. This is because the SIGCONT signal handler
is being executed in that second thread, in parallel to the main thread.
The fix here will be to change the SIGCONT signal handler to redirect its
execution to the main thread.
What to do with the other signal handlers, I have not yet thought about.
But in any case, signals and multithreading together is one of the most
complicated areas of POSIX programming. Any hope to get this done just
by calling pthread_sigmask() is day-dreaming.
Bruno
/* Delaying the delivery of signals to the current process.
Copyright (C) 2026 Free Software Foundation, Inc.
This file is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
/* Written by Bruno Haible. */
#ifndef _SIGDELAY_H
#define _SIGDELAY_H
#include <signal.h>
#ifdef __cplusplus
extern "C" {
#endif
/* While pthread_sigmask() can block the delivery of a signal to a particular
thread and thus ??? in the case of a process with a single thread ??? actually
delay the signal, POSIX lacks a facility for blocking or delaying a signal
process-wide, i.e. for all threads of the process at once.
The sigdelay() function is such a facility.
It assumes that the signal handler for the affected signals does not change
between matching sigdelay (SIG_BLOCK, ...) and sigdelay (SIG_UNBLOCK, ...)
invocations. */
/* With HOW = SIG_BLOCK, this function adds the signals from SET to the set
of delayed signals.
With HOW = SIG_UNBLOCK, this function removes the signals from SET from the
set of delayed signals.
This function works cumulatively, that is, when a signal was added N times,
it also needs to be removed N times in order to restore the initial state.
If OLD_SET is non-NULL, this function also stores the previous set of
delayed signals in *OLD_SET.
Returns 0 if successful, or -1 with errno set in case of failure. */
extern int sigdelay (int how, const sigset_t *restrict set,
sigset_t *restrict old_set);
#ifdef __cplusplus
}
#endif
#endif /* _SIGDELAY_H */
/* Delaying the delivery of signals to the current process.
Copyright (C) 2026 Free Software Foundation, Inc.
This file is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
/* Written by Bruno Haible. */
#include <config.h>
/* Specification. */
#include "sigdelay.h"
#include <errno.h>
#include <stdcountof.h>
#include <stdlib.h>
#include "glthread/lock.h"
#include "thread-optim.h"
#include "sig-handler.h"
/* State regarding a single signal. */
struct state
{
/* Number of times sigdelay (SIG_BLOCK, ...) was invoked for this signal. */
unsigned int count;
/* If count > 0: The original action for the signal. */
struct sigaction saved_action;
/* Whether this signal was caught and is waiting to be re-delivered. */
unsigned int volatile caught;
};
/* The state for all signals.
Size 32 would not be sufficient: On HP-UX, SIGXCPU = 33, SIGXFSZ = 34. */
static struct state states[64];
static _GL_ASYNC_SAFE void
delaying_handler (int sig)
{
if (sig >= 0 && sig < countof (states))
states[sig].caught = 1;
}
/* Lock that makes sigdelay multi-thread safe. */
gl_lock_define_initialized (static, sigdelay_lock)
int
sigdelay (int how, const sigset_t *restrict set, sigset_t *restrict old_set)
{
if (!(how == SIG_BLOCK || how == SIG_UNBLOCK))
{
errno = EINVAL;
return -1;
}
if (old_set != NULL)
sigemptyset (old_set);
bool mt = gl_multithreaded ();
if (mt) gl_lock_lock (sigdelay_lock);
if (old_set != NULL)
for (int sig = 1; sig < countof (states); sig++)
if (states[sig].count > 0)
sigaddset (old_set, sig);
for (int sig = 1; sig < countof (states); sig++)
if (sigismember (set, sig))
{
switch (how)
{
case SIG_BLOCK:
if (states[sig].count == 0)
{
states[sig].caught = 0;
struct sigaction delaying_action;
delaying_action.sa_handler = delaying_handler;
delaying_action.sa_flags = SA_NODEFER;
sigemptyset (&delaying_action.sa_mask);
if (sigaction (sig, &delaying_action,
&states[sig].saved_action) < 0)
abort ();
}
states[sig].count++;
break;
case SIG_UNBLOCK:
if (states[sig].count == 0)
/* Invalid call. */
abort ();
states[sig].count--;
if (states[sig].count == 0)
{
unsigned int caught = states[sig].caught;
if (sigaction (sig, &states[sig].saved_action, NULL) < 0)
abort ();
if (caught)
raise (sig);
}
break;
}
}
if (mt) gl_lock_unlock (sigdelay_lock);
return 0;
}