Linux pidfds in Device Supervisors: Avoid Signalling a Reused PID

Use a pidfd to keep a stable reference to one process lifetime after acquiring its identity correctly. A numeric PID read later from a stale record can identify a different process after reuse. A pidfd prevents that reference from silently retargeting, but it does not repair an acquisition that already selected the wrong process, grant signal permission or replace child reaping.

This guide uses the process-pidfd interfaces documented in Linux man-pages 6.12: pidfd_send_signal appeared in Linux 5.1, pidfd_open in 5.3 and waitid with P_PIDFD in 5.4. Examples use flags zero unless stated otherwise. Check the deployed kernel, libc declarations and security policy separately; having a build-time constant does not establish runtime availability.

Acquire the reference while identity is still protected

For an already running process found through a pathname or PID file, pidfd_open binds to the process currently carrying that PID. If the intended service exited before the call and its number was reused, the reference can be stable but wrong. Establish a trusted launch or service-manager handoff rather than treating a PID file as permanent identity.

For a direct child, the pidfd_open documentation gives a useful fork-then-open guarantee. Even if the child exits quickly, its zombie retains the PID provided SIGCHLD was not explicitly ignored, SA_NOCLDWAIT was not selected, and no other reaper consumed the child. Preserve those conditions until acquisition finishes. Coordinate with the reaper before forking, not after observing a fast exit.

If the application cannot maintain that interval, evaluate creation with CLONE_PIDFD through the appropriate supported clone interface so the process reference is returned with creation. This is a design alternative requiring its own exact ABI review, not permission to paste architecture-specific clone arguments into a supervisor. Keep the launch protocol simple where ordinary fork plus controlled reaping is sufficient.

Three stages show controlled pidfd acquisition, stable process reference and separate child-status collection, with a warning that stale PID-file lookup can select the wrong process before acquisition.
Original lifecycle model. Stable references preserve acquired identity; they cannot authenticate an obsolete PID lookup.

Keep reference, readiness and status separate

A pidfd returned by pidfd_open is close-on-exec and can be polled for process termination. Readiness does not deliver an exit-status byte stream: the documented read operation fails with EINVAL. On a child process, waitid with P_PIDFD retrieves termination information and can reap it. Holding a reference to an unrelated process does not make it your child or authorize waiting for its status.

Use WEXITED for termination and interpret si_code before si_status. A normal exit status and a terminating signal are different data. With WNOHANG, successful return alone does not mean a child result was returned; initialize and check the result fields under the documented contract. WNOWAIT leaves the result waitable and therefore requires an eventual consuming wait by the designated owner.

PIDFD_NONBLOCK, available since Linux 5.10, affects waiting on a still-running referenced process: waitid can return EAGAIN rather than block. It does not convert the pidfd into a readable status payload. Keep ordinary flags-zero operation and this optional behavior in separate compatibility tests.

Signal the reference, then confirm the outcome

pidfd_send_signal targets the referenced process subject to normal permission and namespace restrictions. A zero signal can check the permitted existence path without delivering a signal. Successful delivery of a real termination request would still not prove the process exited; confirm through the reference’s readiness and the appropriate status path.

Distinguish EPERM from ESRCH and from unavailable interfaces. EPERM calls for a permission or policy diagnosis, not a numeric-PID retry. ESRCH means the referenced target is no longer available for the operation; replacing the call with kill on an old PID can reintroduce the exact reuse problem the reference solved. Closing the pidfd releases a descriptor reference; it does not itself request termination.

State Supervisor action
Identity acquired, child running Retain pidfd with launch generation; monitor and apply authorized control policy.
Termination readiness Collect the eligible child’s result through the single wait owner.
Result consumed Record terminal state, remove event registration and release the descriptor.
Unsupported or denied operation Use a prequalified alternative or report supervision unavailable; do not weaken identity silently.

Design the compatibility path explicitly

Inventory all deployed combinations of kernel, libc and seccomp policy. A missing libc convenience wrapper can be handled in a reviewed platform abstraction using the target’s syscall definitions, but an ENOSYS result still needs qualification. Security policy can deliberately return an error resembling an absent syscall. Record the operation and policy context instead of inferring kernel age from one error.

For older targets, a direct-child supervisor can retain traditional wait ownership and carefully coordinate signals while the child’s identity remains protected by that lifecycle. A general stale PID-file service controller cannot obtain equivalent guarantees merely by checking the PID twice. Reading a start-time field and then sending a numeric-PID signal leaves a check-to-use interval. If stable identity is required and no supported path exists, fail the operation visibly.

Use bounded owned-child tests

An executed host fixture on x86-64 Linux 6.18.44 created one child blocked on a private pipe. The parent opened its pidfd before allowing exit, observed no immediate termination readiness, and successfully performed a zero-signal probe. After the parent released the pipe, the child exited with status 7; polling became ready and waitid(P_PIDFD) collected that status. A subsequent zero-signal probe returned ESRCH.

No terminating signal was sent, no unrelated PID was targeted, and no attempt was made to force PID reuse. This verifies the observed host lifecycle, not the minimum-kernel boundary or a target security policy. Proposed additional tests cover immediate child exit, acquisition failure, a competing reaper detected as a design violation, and descriptor exhaustion with complete cleanup.

The engineering deliverable is an identity-acquisition diagram and terminal-state ledger. It should show who may reap, when the pidfd becomes authoritative, which failures preserve safety, and how descriptor ownership ends. That record is more useful than a single successful signal test because the dangerous mistakes occur at creation, exit and reuse boundaries.

Similar Posts