Linux Parent-Death Signals: Arm-After-Fork Races and Supervisor Lifetime Boundaries
Use Linux parent-death signaling with explicit race checks, creator-thread lifetime, signal policy, exec boundaries, and tests that prove child shutdown.
A helper can outlive the process that launched it. Sometimes that is deliberate, as with a daemon. Sometimes it is a defect: an interrupted build controller leaves a worker processing an obsolete job, or a local supervisor crashes while its child continues holding resources. Linux’s PR_SET_PDEATHSIG gives a child a signal when its relevant parent lifetime ends, but it does not automatically establish the complete supervision contract.
The difficult boundaries are event timing, creator-thread identity, inherited signal policy, and changes across fork() or exec(). A few lines that arm a signal can look reassuring while still missing a parent that already died. This article uses the upstream Linux man-pages prctl documentation together with the Linux 6.10 kernel implementation to make those boundaries explicit.
Define which parent lifetime matters
The parent for this interface is the thread that created the child process, not simply “the whole executable named in ps.” The manual explicitly warns that termination of that creator thread can trigger the signal even while other threads in the parent process continue running.
This matters for a thread-pool launcher. A short-lived thread can create a child, return through a thread-termination path, and accidentally end the child’s intended lifetime. The fact that the supervisor’s main thread is still alive does not repair that ownership decision. Launch from a thread whose lifetime matches the worker contract, or choose a supervision design that does not depend on creator-thread death.
The signal is process-directed. Its ultimate effect depends on the child’s signal dispositions and mask. SIGTERM is a request with configurable handling, not an unconditional destructor callback. An ignored signal, blocked signal, or handler that deliberately continues can prevent the expected shutdown. Review signal policy separately from the successful prctl() result.
The event is not retrospective
Setting PR_SET_PDEATHSIG configures notification for subsequent events. If the relevant parent and ancestor subreapers have already terminated before the child arms the setting, the kernel does not synthesize a notification merely because the process is now orphaned.
The common race begins at fork(): the parent creates the child and then exits before the child reaches its setup code. Calling prctl() in the child succeeds, but success does not mean the original parent was still alive when it was armed. Capturing getppid() only after arming can identify the replacement parent instead of the launcher.
For a deliberately simple, single-threaded parent, capture the parent’s PID before the fork and give that expected identity to the child’s setup. Arm the signal, then compare getppid() against the recorded PID. A changed parent identity is a reason for the child to stop rather than continuing an obsolete job.
A narrow setup helper
The following illustrative helper must be called by the child immediately after a fork from a single-threaded parent. The caller supplies the PID recorded by that parent before the fork. The variadic prctl() arguments use explicit unsigned long widths rather than relying on the type of an integer signal constant.
#include <sys/types.h>
#include <sys/prctl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
int arm_parent_death(pid_t expected_parent)
{
if (expected_parent <= 0) {
errno = EINVAL;
return -1;
}
if (prctl(PR_SET_PDEATHSIG, (unsigned long)SIGTERM,
0UL, 0UL, 0UL) == -1)
return -1;
if (getppid() != expected_parent) {
errno = ESRCH; /* application-defined setup failure */
return -1;
}
return 0;
}
The launcher-side contract is:
single-threaded parent records its own getpid()
parent calls fork()
child calls arm_parent_death(recorded_parent_pid)
child uses _exit() on any setup failure
child starts useful work only after setup succeeds
This is not a complete process launcher. The child must establish the intended signal disposition and mask, handle all other setup failures, and use an appropriate post-fork path. No native Linux execution is claimed for this helper during authoring. The ESRCH assignment is the helper’s diagnostic convention for changed parent identity, not a claim that the kernel returned that error.
If the parent dies after arming but before the identity comparison, the configured signal can arrive during setup. That is part of the desired fail-closed behavior when the chosen signal policy terminates the child. Do not add a handler that suppresses this termination without providing a replacement shutdown protocol.
A PID comparison does not solve the multithreaded gap
The identity check above cannot detect every creator-thread transition. If the creator thread dies before arming while another thread keeps the parent process alive, getppid() can still equal the original process PID. The earlier thread-death event has already been missed, yet the process identity comparison succeeds.
This is why the single-threaded scope is essential rather than a footnote. A multithreaded service needs an explicitly reviewed launcher-thread lifetime or a different liveness protocol. A control connection, heartbeat, or readiness handshake may provide the intended ownership signal, but its descriptor inheritance, closure, timeout, and reconnect rules also need design. Replacing one unchecked assumption with another is not a fix.
PID namespaces introduce another identity boundary. Record and compare identifiers in the relevant namespace and treat unexpected parent visibility as setup failure. Do not accept a PID copied from an unrelated host namespace or persist a numeric PID as a durable owner reference.
Fork, exec, and credentials can change the setting
The setting is cleared in a newly forked child. A worker that creates another process must not assume its parent-death setting automatically propagates. Each process requires its own reviewed setup and ownership contract. The pinned fork.c implementation independently shows the new task’s pdeath_signal reset.
The manual describes preservation across an ordinary execve(), but clearing for privilege-changing executions such as set-user-ID, set-group-ID, or file-capability programs. Credential changes can also clear the setting. The inspected kernel credential and exec code provides concrete evidence for these transitions.
If a worker changes identity or executes a different program, verify the state at that boundary rather than asserting that the original setup protects every future image. PR_GET_PDEATHSIG reports the current configured signal. An unchanged numeric setting is useful configuration evidence, but it still does not prove the future signal will terminate a process whose dispositions or mask have changed.
Ancestor subreapers are also part of the documented behavior. After reparenting to a subreaper, later termination of that subreaper can generate the configured parent-death signal. Do not describe the interface as an event permanently bound only to the initial launcher PID in every subsequent lifecycle state.
Notification is not tree cancellation or reaping
Parent-death signaling concerns the configured process. It does not automatically terminate all descendants, revoke every inherited descriptor, remove temporary files, or roll back partially committed work. A child can spawn a grandchild that needs an independent lifecycle policy.
Nor does a termination signal reap a child. Reaping belongs to the surviving parent or adopter’s wait protocol. Keep cancellation, application cleanup, process exit, and status collection as separate events in the supervisor design. A child that exits quickly can still expose defective status handling elsewhere.
For nontransactional work, design an interruption-safe output protocol. A partially written artifact must not become an accepted result merely because a worker stopped when its supervisor died. Record job identity and publish validated results through the application’s ordinary commit boundary.
Verify timing, not just configuration
Build a disposable Linux fixture with explicit synchronization points before arming, after arming, and before useful work. Exercise parent exit before setup, parent exit after setup, and normal completion while the parent remains alive. Prove child termination through process/status observation or an independent test supervisor, not only through a printed “armed” message.
Add cases for an ignored signal, a blocked signal, ordinary exec, credential transitions, and a creator thread that exits while the parent process remains alive. These cases establish the limits that a happy-path test hides. Use synthetic work and private scratch resources so deliberately killing a controller cannot damage real data.
Accept the mechanism only when the chosen event matches the product’s owner lifetime and the tested shutdown behavior matches its requirements. If it does not, restore the known-good supervision design. Parent-death signaling is a useful Linux primitive, not a substitute for a complete lifecycle and recovery protocol.
Related:
- Linux signalfd: Consume Signals in an Event Loop Without Async Handlers
- Fixing Zombie Processes That Won’t Clear from the Process Table
Sources: