(Sorry for the long post but I wanted to be as accurate as possible)
I was trying to print the signal mask of the main thread while writing a C program, when I came across something strange regarding how the sigprocmask
function works.
Background [Source: manual page sigprocmask(2)
]
The sigprocmask
function is used to fetch and/or change the signal mask of the calling thread.
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- If
oldset
is non-NULL
, the previous value of the signal mask is stored inoldset
. - If
set
isNULL
, then the signal mask is unchanged (i.e.,how
is ignored), but the current value of the signal mask is nevertheless returned inoldset
(if it is notNULL
). - A set of functions for modifying and inspecting variables of type
sigset_t
("signal sets") is described insigsetops(3)
. In example:int sigemptyset(sigset_t *set);
: initializes the signal set given by set to empty, with all signals excluded from the set.int sigfillset(sigset_t *set);
: initializes set to full, including all signals.int sigismember(const sigset_t *set, int signum);
: tests whether signum is a member of set.
Note: When creating a filled signal set, the glibc sigfillset
function does not include the two real-time signals used internally by the NPTL threading implementation.
System specifics
Linux Distribution: Linux Mint 19.3
Cinnamon
Glibc version: 2.27
(default)
Also verified for Glibc version: 2.31.9
Output of uname -a
:
Linux 5.0.0-32-generic #34~18.04.2-Ubuntu SMP Thu Oct 10 10:36:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Problem Replication
The program that alarmed me of something possibly going wrong is the following:
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 32
#define OUTOFBOUNDS
#undef OUTOFBOUNDS
void print_set_bin(sigset_t *setp);
int main(void)
{
sigset_t set;
printf("NSIG = %d\n\n", NSIG);
printf("Empty set:\n");
if (sigemptyset(&set))
{
perror("sigemptyset");
return -1;
}
print_set_bin(&set);
printf("Filled set:\n");
if (sigfillset(&set))
{
perror("sigfillset");
return -1;
}
print_set_bin(&set);
printf("After sigprocmask():\n");
if (sigprocmask(SIG_BLOCK, NULL, &set))
{
perror("sigprocmask");
return -1;
}
print_set_bin(&set); // Why non-empty?
return 0;
}
void print_set_bin(sigset_t *setp)
{
int sig, res;
char buff[BUFFER_SIZE];
if (!setp)
{
fprintf(stderr, "print_set_bin(): NULL parameter\n");
return;
}
#ifdef OUTOFBOUNDS
for (sig = 0; sig <= NSIG; sig++)
#else
for (sig = 1; sig < NSIG; sig++)
#endif
{
res = sigismember(setp, sig);
if (res == -1)
{
snprintf(buff, BUFFER_SIZE, "sigisimember [%d]", sig);
perror(buff);
}
else
printf("%d", res);
}
printf(" [%s]\n\n", sigisemptyset(setp) ? "Empty" : "Non-empty");
}
Function print_set_bin
prints the output of sigismember
(0
for not a member, 1
for a member) for all signals. Macro definition NSIG
(= 65) in signal.h
is the biggest signal number plus one (1) as mentioned in /usr/include/x86_64-linux-gnu/bits/signum-generic.h
. In the same file, it is also mentioned that this biggest signal number includes real-time signals (number range [32, 64]) and that signal number zero (0) is reserved for testing purposes.
As a result, my code posted above tests for signal numbers that belong in range [1, 64].
Below follows the output of the program:
NSIG = 65
Empty set:
0000000000000000000000000000000000000000000000000000000000000000 [Empty]
Filled set:
1111111111111111111111111111111001111111111111111111111111111111 [Non-empty]
After sigprocmask():
0000000000000000000000000000000000000000000000000000000000000000 [Non-empty]
Output explantion
In this program, variable set
of type sigset_t
is manipulated. At first, function sigemptyset
is used to set all bits to zero, then function sigfillset
is used to set all bits to one (except from two; see Note in Background section) and finally sigprocmask
is used to store current signal mask to the same variable. After all operations taking place on the signal set, function print_set_bin
is used to print which signals belong to the set and whether the set is empty (using sigisemptyset()
).
The problem seems to be the last call to print_set_bin
, where no signal is found belonging to the set, but the sigisemptyset
function characterizes the set as non-empty. That got me thinking whether sigset_t
holds more than 64 bits and at least one of them is non-zero.
Research
Tracing the header files included in signal.h
, I found that sigset_t
is a struct defined in /usr/include/x86_64-linux-gnu/bits/types/__sigset_t.h
as __sigset_t.h
and typedef
ed in /usr/include/x86_64-linux-gnu/bits/types/sigset_t.h
:
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
At first I thought that 1024 bits were too many, but I then came across with this answer to another unix.stackexchange.com question.
I then decided to use the implementation details of the sigset_t
struct to print all the 1024 bits. I did it in the code following, by replacing function print_set_bin
with function print_set_word
that prints all _SIGSET_NWORDS
(= 16) unsigned long int
s allocated to __val
array.
void print_set_word(sigset_t *setp)
{
int i;
if (!setp)
{
fprintf(stderr, "print_set_word(): NULL parameter\n");
return;
}
for (i = 0; i < 16; i++)
{
printf("%lu\n", setp->__val[i]);
}
printf("[%s]\n\n", sigisemptyset(setp) ? "Empty" : "Non-empty");
}
Program output:
Empty set:
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
[Empty]
Filled set:
18446744067267100671
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
[Non-empty]
After sigprocmask():
0
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
[Non-empty]
Notes:
18446744067267100671
==0b1111111111111111111111111111111001111111111111111111111111111111
(64 bits set to 1 except from two; see Note in Background section)18446744073709551615
==0b1111111111111111111111111111111111111111111111111111111111111111
(64 bits set to 1)
Output Explanation and Question
As you can see, sigemptyset()
and sigfillset()
manipulate all the 1024 bits of the set. Calling sigemptyset()
instead of sigfillset()
before calling sigprocmask()
reveals thatsigprocmask()
manipulates only the first 64 bits (one unsigned long int
), leaving the remaining 1024-64=960 bits untouched! And here comes the long-waited question: Isn't this a bug? Shouldn't sigprocmask()
write to the whole struct data?
sizeof(sigset_t)
equals correctly to128
Bytes. My question is about thesigprocmask
function that doesn't manipulate all the 128 bytes insigset_t
. It is a whole different thing.