signals: a blocked signal stays pending instead of killing

sigsend() ended a process for a default-fatal signal (SIGUSR1, SIGTERM,
SIGINT...) on arrival, whatever its signal mask said. Blocking is
exactly the promise that it will not: the signal waits until it is
unblocked, or until the program takes it some other way -- which is
what signalfd is, and how this was found.

A blocked signal is now only made pending. If it is later unblocked
while still pending, deliver_signals() applies the default action
then. SIGKILL cannot be blocked, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 10:59:52 +02:00
commit 6bfb367090
3 changed files with 40 additions and 12 deletions

View file

@ -1508,6 +1508,34 @@ init_ignores(struct proc *p, int sig)
return p == initproc && p->sig_handler[sig] == 0;
}
// Whether a signal's default action is to end the process.
int
sig_default_kills(int sig)
{
return sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM;
}
// Whether sending `sig` ends `p` now: no handler, a default action that
// kills, and **not blocked**. A blocked signal stays pending, which is the
// whole of what blocking means -- and the contract signalfd is built on: a
// program blocks SIGUSR1 and reads it from a descriptor instead. This used to
// kill on arrival whatever the mask said. deliver_signals() applies the
// default action if the signal is unblocked later, still pending.
// SIGKILL cannot be blocked.
static int
sig_kills_now(struct proc *p, int sig)
{
if(sig == SIGKILL)
return 1;
if(p->signal_mask & (1ULL << sig))
return 0;
return p->sig_handler[sig] == 0 && sig_default_kills(sig);
}
int
sigsend(int pid, int sig)
{
@ -1524,12 +1552,7 @@ sigsend(int pid, int sig)
return 0; // silently dropped, as POSIX requires
}
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM))
if(sig_kills_now(p, sig))
p->killed = 1;
if(sig == SIGKILL)
p->killed = 1;
@ -1573,12 +1596,7 @@ sigsend_pgrp(int pgid, int sig)
if(init_ignores(p, sig))
continue; // see init_ignores(): this is the Ctrl-C case
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM)){
if(sig_kills_now(p, sig)){
p->killed = 1;
if(p->killsig == 0)
p->killsig = sig;

View file

@ -191,8 +191,17 @@ deliver_signals(struct trapframe *tf)
// applied by sigsend(); all that is left here is to stop it being
// pending, which the old `continue` never did, so an ignored signal
// was re-examined on the way out of every syscall forever after.
//
// Except for a signal that was blocked when it arrived: sigsend()
// left it pending without acting, and now that it is deliverable its
// default action happens here.
if (myproc()->sig_handler[sig] <= 1) {
myproc()->pending_signals &= ~(1ULL << sig);
if (myproc()->sig_handler[sig] == 0 && sig_default_kills(sig)) {
myproc()->killed = 1;
if (myproc()->killsig == 0)
myproc()->killsig = sig;
}
continue;
}