signals: Bug or manpage inconsistency?

From: Thomas Gleixner
Date: Tue May 30 2017 - 09:21:56 EST


While trying to address the longstanding FIXME in the posix timer code
related to ignored signals, I stumbled over the following issue:

I blocked the signal of the timer, then installed the SIG_IGN handler,
created and started the timer. After a short sleep the timer has fired
several times, but it's still ignored AND blocked.

Calling sigpending() after that has the timer signal set. See test case
below.

But 'man sigpending' says:

"If a signal is both blocked and has a disposition of "ignored", it is _not_
added to the mask of pending signals when generated."

So something is clearly wrong here.

The same happens with sigwait() while the signal is still blocked and
ignored, it returns with that signal number and has the signal dequeued.


The whole blocked vs. ignored handling is inconsistent both in the posix
spec and in the kernel.

The only thing vs. ignored signals what the spec mandates is:

SIG_IGN:

Delivery of the signal shall have no effect on the process.

...

Setting a signal action to SIG_IGN for a signal that is pending shall
cause the pending signal to be discarded, whether or not it is blocked.

...

Any queued values pending shall be discarded and the resources used to
queue them shall be released and made available to queue other signals.

That's exactly what the kernel does in do_sigaction().

And for everything else the spec is blurry:

If the action associated with a blocked signal is to ignore the signal
and if that signal is generated for the process, it is unspecified
whether the signal is discarded immediately upon generation or remains
pending.

So the kernel has chosen to keep them pending for whatever reasons, which
does not make any sense to me, but there is probably a historic reason.

The commit which added the queuing of blocked and ignored signals is in the
history tree with a pretty useless changelog.

https://git.kernel.org/pub/scm/linux/kernel/git/tglx/history.git

commit 98fc8ab9e74389e0c7001052597f61336dc62833
Author: Linus Torvalds <torvalds@xxxxxxxxxxxxxxxxxxxxx>
Date: Tue Feb 11 20:49:03 2003 -0800

Don't wake up processes unnecessarily for ignored signals

It rewrites sig_ignored() and adds the following to it:

+ /*
+ * Blocked signals are never ignored, since the
+ * signal handler may change by the time it is
+ * unblocked.
+ */
+ if (sigismember(&t->blocked, sig))
+ return 0;

I have no idea how that is related to $subject of the commit and why this
decision was made.

Linus, any recollection?

IMO, it's perfectly reasonable to discard ignored signals even when the
signal is in the blocked mask. When its unblocked and SIG_IGN is replaced
then the next signal will be delivered. But hell knows, how much user space
depends on this weird behaviour by now.

Thanks,

tglx

8<-------------

#define _GNU_SOURCE
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <time.h>

#include <sys/types.h>
#include <sys/time.h>
#include <sys/syscall.h>

int main(void)
{
struct itimerspec tspec;
struct sigevent sigev;
struct sigaction action;
int signal, sig = SIGALRM;
sigset_t set, pend;
timer_t timerid;

sigemptyset(&set);
sigaddset(&set, sig);
sigprocmask(SIG_BLOCK, &set, NULL);

memset(&action, 0, sizeof(action));
action.sa_handler = SIG_IGN;
sigaction(sig, &action, NULL);

memset(&sigev, 0, sizeof(sigev));
sigev.sigev_notify = SIGEV_SIGNAL;
sigev.sigev_signo = sig;
sigev.sigev_value.sival_ptr = &timerid;
timer_create(CLOCK_REALTIME, &sigev, &timerid);

tspec.it_interval.tv_sec = 0;
tspec.it_interval.tv_nsec = 100 * 1e6;

tspec.it_value.tv_sec = 0;
tspec.it_value.tv_nsec = 100 * 1e6;

timer_settime(timerid, 0, &tspec, NULL);

sleep(1);

sigpending(&pend);
if (sigismember(&pend, sig)) {
/* This is reached */
printf("Timer signal pending 1\n");
sigwait(&set, &signal);
printf("sigwait signal: %d\n", signal);
}

sleep(1);

printf("Unblock\n");
sigprocmask(SIG_UNBLOCK, &set, NULL);

sigpending(&pend);
if (sigismember(&pend, sig)) {
/* This is not reached */
printf("Timer signal pending 2\n");
sigwait(&set, &signal);
printf("sigwait signal: %d\n", signal);
}

sleep(1);

sigpending(&pend);
if (sigismember(&pend, sig)) {
/* This is not reached */
printf("Timer signal pending 3\n");
sigwait(&set, &signal);
printf("sigwait signal: %d\n", signal);
}

timer_delete(timerid);
return 0;
}