krallin/tini

How come tini proxies signals to child processes if the sigcgt bits are all 0?

chengdol opened this issue · 1 comments

Hi folks,

Thanks in advance to take a look at my question.

I run tini in my docker container as PID 1 and I see inside the container:

docker run -it --rm <docker with init> bash

cat /proc/1/status | grep -i sig

SigPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000300000
SigCgt: 0000000000000000

I know that in Linux init process is treated specially, in its PID namespace the init process will ignore any signal with default action (not talk about signal sent from parent PID namespace).

The SigCgt is all 0 which means there is no signal handler registered. If so how come tini redirects signal to its child if it does not catch any signal? I think I may misunderstand the relationship between sigcgt and signal forwarding, appreciate your kindness to correct me if I am wrong.

The way Tini catches signals is by blocking all signals that should be forwarded to the child, and then waiting for them via sigtimedwait (https://linux.die.net/man/2/sigtimedwait). The set of signals Tini blocks is all signals save for things like SEGV that would be used by the Kernel to report errors in Tini's execution to Tini itself:

The blocking happens here:

tini/src/tini.c

Lines 456 to 499 in 378bbbc

int configure_signals(sigset_t* const parent_sigset_ptr, const signal_configuration_t* const sigconf_ptr) {
/* Block all signals that are meant to be collected by the main loop */
if (sigfillset(parent_sigset_ptr)) {
PRINT_FATAL("sigfillset failed: '%s'", strerror(errno));
return 1;
}
// These ones shouldn't be collected by the main loop
uint i;
int signals_for_tini[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU};
for (i = 0; i < ARRAY_LEN(signals_for_tini); i++) {
if (sigdelset(parent_sigset_ptr, signals_for_tini[i])) {
PRINT_FATAL("sigdelset failed: '%i'", signals_for_tini[i]);
return 1;
}
}
if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, sigconf_ptr->sigmask_ptr)) {
PRINT_FATAL("sigprocmask failed: '%s'", strerror(errno));
return 1;
}
// Handle SIGTTIN and SIGTTOU separately. Since Tini makes the child process group
// the foreground process group, there's a chance Tini can end up not controlling the tty.
// If TOSTOP is set on the tty, this could block Tini on writing debug messages. We don't
// want that. Ignore those signals.
struct sigaction ign_action;
memset(&ign_action, 0, sizeof ign_action);
ign_action.sa_handler = SIG_IGN;
sigemptyset(&ign_action.sa_mask);
if (sigaction(SIGTTIN, &ign_action, sigconf_ptr->sigttin_action_ptr)) {
PRINT_FATAL("Failed to ignore SIGTTIN");
return 1;
}
if (sigaction(SIGTTOU, &ign_action, sigconf_ptr->sigttou_action_ptr)) {
PRINT_FATAL("Failed to ignore SIGTTOU");
return 1;
}
return 0;
}

And the waiting happens there:

tini/src/tini.c

Lines 501 to 514 in 378bbbc

int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) {
siginfo_t sig;
if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) {
switch (errno) {
case EAGAIN:
break;
case EINTR:
break;
default:
PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno));
return 1;
}
} else {

Now, you might be left wondering why SigBlk is all zeroes if we're supposedly blocking all signals. The answer to that is that while sigtimedwait is executing, the Kernel temporarily unblocks all signals we are waiting on:

https://github.com/torvalds/linux/blob/a76c3d035872bf390d2fd92d8e5badc5ee28b17d/kernel/signal.c#L3581-L3601

As a consequence, if you look at the signals blocked by Tini while Tini is running, then you'll typically see nothing, because most of the time Tini is doing nothing and is just waiting for signals. That said, if you get particularly lucky, you could see a different signal mask if you check the signals while Tini is reaping children.