Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

kernel: prevent prctl(PR_SET_PDEATHSIG) from racing with parent process exit

If a process calls prctl(PR_SET_PDEATHSIG) at the same time that the
parent process exits, the child will write to me->pdeath_sig at the same
time the parent is reading it. Since there is no synchronization, this is
a data race.

Worse, it is possible that a subsequent call to getppid() can continue to
return the previous parent process ID without the parent death signal
being delivered. This happens in the following scenario:

parent child

forget_original_parent() prctl(PR_SET_PDEATHSIG, SIGKILL)
sys_prctl()
me->pdeath_sig = SIGKILL;
getppid();
RCU_INIT_POINTER(t->real_parent, reaper);
if (t->pdeath_signal) /* reads stale me->pdeath_sig */
group_send_sig_info(t->pdeath_signal, ...);

And in the following:

parent child

forget_original_parent()
RCU_INIT_POINTER(t->real_parent, reaper);
/* also no barrier */
if (t->pdeath_signal) /* reads stale me->pdeath_sig */
group_send_sig_info(t->pdeath_signal, ...);

prctl(PR_SET_PDEATHSIG, SIGKILL)
sys_prctl()
me->pdeath_sig = SIGKILL;
getppid(); /* reads old ppid() */

As a result, the following pattern is racy:

pid_t parent_pid = getpid();
pid_t child_pid = fork();
if (child_pid == -1) {
/* handle error... */
return;
}
if (child_pid == 0) {
if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) {
/* handle error */
_exit(126);
}
if (getppid() != parent_pid) {
/* parent died already */
raise(SIGKILL);
}
/* keep going in child */
}
/* keep going in parent */

If the parent is killed at exactly the wrong time, the child process can
(wrongly) stay running.

I didn't manage to reproduce this in my testing, but I'm pretty sure the
race is real. KCSAN is probably the best way to spot the race.

Fix the bug by holding tasklist_lock for reading whenever pdeath_signal is
being written to. This prevents races on me->pdeath_sig, and the locking
and unlocking of the rwlock provide the needed memory barriers. If
prctl(PR_SET_PDEATHSIG) happens before the parent exits, the signal will
be sent. If it happens afterwards, a subsequent getppid() will return the
new value.

Link: https://lkml.kernel.org/r/20250913-fix-prctl-pdeathsig-race-v1-1-44e2eb426fe9@gmail.com
Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
Cc: Oleg Nesterov <oleg@redhat.com>
Cc: Mateusz Guzik <mjguzik@gmail.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>

authored by

Demi Marie Obenour and committed by
Andrew Morton
634cdfd6 74058c0a

+10
+10
kernel/sys.c
··· 2488 2488 error = -EINVAL; 2489 2489 break; 2490 2490 } 2491 + /* 2492 + * Ensure that either: 2493 + * 2494 + * 1. Subsequent getppid() calls reflect the parent process having died. 2495 + * 2. forget_original_parent() will send the new me->pdeath_signal. 2496 + * 2497 + * Also prevent the read of me->pdeath_signal from being a data race. 2498 + */ 2499 + read_lock(&tasklist_lock); 2491 2500 me->pdeath_signal = arg2; 2501 + read_unlock(&tasklist_lock); 2492 2502 break; 2493 2503 case PR_GET_PDEATHSIG: 2494 2504 error = put_user(me->pdeath_signal, (int __user *)arg2);