FreeBSD procctl: Reapers, Parent-Death Signals, and Process Hardening
A practical map of FreeBSD procctl process controls, including reaper trees, parent-death signals, mitigation state, permissions, and safe operations.
FreeBSD’s procctl(2) lets a program query or change selected behavior of a process or process group. Its controls cover several different jobs: supervising descendants with process reapers, delivering a signal when a parent dies, and enabling or querying hardening properties such as ASLR, trace restrictions, stack-gap behavior, and write-xor-execute policy where the running release supports them.
The common name hides an important distinction. procctl is the C system call. The general command-line utility is proccontrol(1), and protect(1) is a specialized utility for swap-exhaustion protection. Scripts should use the documented utility that exposes the desired control rather than assuming there is a /usr/bin/procctl command mirroring every kernel operation.
The target is an id type plus an id
The system call has the shape procctl(idtype, id, cmd, data). An idtype such as P_PID or P_PGID determines how to interpret the numeric identifier. Some operations can target several matching processes; the manual describes these as best-effort operations, so callers must not assume one return value means an atomic group-wide transition.
Permissions depend on the operation. Querying status generally requires the ability to observe the process. Changing security-relevant state generally requires debugging authority over it. Ownership alone should not be used as a substitute for checking the actual error result, especially in a jail, capability-constrained process, or environment with additional MAC policy.
Controls also vary by FreeBSD release and architecture. Code should compile-time guard constants when portability matters and treat EINVAL or an unsupported status as a version signal, not as proof that the kernel accepted an equivalent default.
A process reaper owns an explicit descendant tree
A process can acquire reaper status with PROC_REAP_ACQUIRE. Descendants in that reaper tree that become orphaned are reparented to the reaper rather than directly to PID 1. This gives a service supervisor, jail manager, or container-like runtime a reliable subtree to observe and collect.
The facility includes operations to query reaper status, enumerate process information, send signals into the tree, and release reaper ownership. It is more structured than walking a snapshot of parent PIDs: ordinary parent relationships can change between enumeration and signaling, while the kernel maintains reaper membership.
Reaper status is not a replacement for wait(2). The supervisor still needs to reap exited children and handle SIGCHLD correctly. Nor does it create a security boundary. A reaper organizes lifecycle relationships; jails, credentials, Capsicum, MAC policy, and resource controls provide isolation.
A robust supervisor acquires reaper status before it launches workers, records failure to acquire as a startup error, drains child status continuously, and defines escalation from graceful termination to a bounded forced shutdown. It should query the tree again after signaling because children can fork or exit while shutdown is in progress.
Parent-death signals close one lifecycle race
The parent-death-signal controls let a process request a signal if its parent terminates. This is useful for a helper that must not outlive its direct controller. Configure the signal early in child startup and then verify the parent relationship, because a parent can exit between fork() and the child’s procctl() call.
That race cannot be solved by setting a signal and forgetting it. A practical pattern records the expected parent PID, configures the parent-death signal, calls getppid() again, and exits if the relationship already changed. The signal is a notification, not guaranteed cleanup: it can be blocked, caught, or handled while the process is stuck elsewhere.
Do not confuse direct parent death with reaper-tree shutdown. Parent-death signaling ties one child to one parent transition. A reaper can manage a larger descendant graph. Supervisors often use both because they solve different failure paths.
Hardening controls are state, not labels
Depending on the release, procctl exposes controls for address-space layout randomization, maximum memory-map protections, traceability, stack gaps, trap behavior for Capsicum violations, and write-xor-execute enforcement. proccontrol(1) can apply supported settings to an existing PID or launch a command with selected controls.
Each mitigation has compatibility costs. Preventing mappings from becoming more executable can break a JIT compiler. Disabling tracing impairs a debugger and may change crash collection. ASLR behavior can interact with binaries carrying explicit feature notes. Test the actual executable and its plugins, not only a trivial process.
Query the resulting state after launch and expose it in diagnostics. A configuration file that requests a mitigation is not evidence that the live process has it, especially across an OS upgrade or architecture change. For services, place the invocation in version-controlled startup configuration and verify it in an integration test.
Swap-exhaustion protection can hurt the whole host
FreeBSD can mark a process as protected from being selected for termination when the system exhausts swap. The protect(1) utility exposes that mechanism and can arrange inheritance for descendants. It is intended for critical processes whose loss would make recovery harder.
Protection is not a resource reservation. A protected runaway process can continue consuming memory while the kernel kills less critical work, and the manual warns that protecting the wrong process can contribute to deadlock. Apply it narrowly, combine it with rctl(8) limits where appropriate, and monitor actual memory consumption.
The recovery shell, login service, or essential storage process may justify careful protection. A large application merely labeled “important” usually needs bounded memory and restart policy more than immunity from the last-resort killer.
Build tests around failure and identity changes
For a reaper, create a child that forks a grandchild and exits. Verify the grandchild enters the expected reaper tree, enumerate it through the supported operation, signal the tree, and reap every exit. Repeat during concurrent forking to exercise the supervisor’s rescan logic.
For a parent-death signal, kill the parent at several points during child startup and confirm the child never becomes an unintended long-lived orphan. For hardening state, query the live process, attempt the operation that should be prohibited, and confirm the exact error or signal. Run those tests under the production user, jail, and service manager.
Log the FreeBSD release, command, target identity, requested value, returned status, and subsequent query. Avoid logging sensitive process arguments or memory. This makes an unsupported feature distinguishable from a permission failure and from a control that was later changed.
procctl is valuable because it turns several lifecycle and mitigation choices into explicit kernel state. It is safe only when each control has a precise purpose, a live-state check, and a tested failure path. Reaper ownership, parent death, process hardening, and memory protection should remain separate decisions even though one interface can express all of them.
Related:
- Capsicum Beyond the Basics: Building Capability-Mode Services From Scratch
- Understanding FreeBSD’s Random Number Subsystem (random(4)) and Entropy Harvesting
Sources: