Repository navigation
Health sentinel: bind the Linux kill to the proxy with pidfd #6987
Description
Area
Service lifecycle
What are you trying to accomplish?
On Linux, the opt-in service health sentinel (serviceHealthRecovery.enabled, #6979, issue #6671) should terminate a hung but listening proxy with a kill that is bound to that exact process, as it already is on Windows.
What prevents this today?
On POSIX the sentinel checks its live process.ppid and then sends SIGKILL to the numeric pid. If the sentinel is descheduled between those two calls while the proxy exits, is reaped, and a new process receives the same pid, the signal reaches the wrong process. The window is small but unbounded. Bun exposes no pidfd or process-handle API, so #6979 documents this as an accepted residual for a default-off feature (merge sign-off on #6979).
What should OpenCodex do?
On Linux kernels that support it (5.3+ for pidfd_open, 5.1+ for pidfd_send_signal), the sentinel opens a pidfd for its parent at startup, re-checks the parent relationship once the fd is held, and terminates through pidfd_send_signal(SIGKILL). A pidfd keeps naming the original process even if its pid is reused. Kernels without pidfd keep today's behaviour. macOS has no pidfd and keeps the documented residual.
Example usage or interface
No new configuration. With "serviceHealthRecovery": { "enabled": true } on Linux, the sentinel log adds a line such as watching pid 4242 on port 10100 (pidfd), and a recovery terminates through the pidfd. Implementation sketch: bun:ffi on libc syscall (SYS_pidfd_open 434, SYS_pidfd_send_signal 424 on x86_64 and aarch64), mirroring src/service/health-sentinel-windows.ts.
Alternatives or workarounds
Leave the probabilistic binding as documented in structure/ops/service-health-recovery.md. Alternatively, have the service manager own liveness (for example systemd WatchdogSec with sd_notify), which needs an in-process notifier and would stall together with the event loop it reports on.
Additional context
Follow-up to #6979 and #6671. The Windows path holds a kernel32 process handle opened at startup (bun:ffi) and is identity-bound already.
Checks
- I searched existing issues and documentation.
- This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
- I removed secrets and personal data.
Activity
fix(service): discard stale sentinel evidence and count future-dated …
fix(service): discard stale sentinel evidence and count future-dated …
fix(service): opt-in health sentinel recovers a hung but listening se…
Ingwannu commentedon Oct 12, 2026
The current dev POSIX path still checks
process.ppidand then signals the numeric target PID (src/service/health-sentinel-main.ts:407,416); #6979 did not make that sequence atomic. This follow-up therefore targets a real documented residual, not a demonstrated fix for #6671's stall cause.For an owner-approved Linux implementation, keep the existing default-off/managed-service scope and reuse the sentinel's process-owner cleanup seam. A held kernel identity must be acquired before arming recovery, parent identity rechecked after acquisition, and the descriptor closed on every teardown/refusal. Specify unsupported syscall/kernel/architecture behavior separately from unexpected permission/resource errors; do not silently turn an unexpected binding failure into permission to kill by PID. Cover parent exit/reparenting during acquisition, PID reuse, already-exited targets, repeated recovery, cancellation/EOF and descriptor cleanup with injected syscall fixtures. macOS retains its existing documented limitation.
This is scoped source triage, not process-termination/boundary acceptance. No FFI/syscall, live kill, restart, provider call or security scan was executed. I am leaving implementation/acceptance of this lifecycle extension to the normal owner review rather than modifying a live sentinel.
lidge-jun commentedon Oct 13, 2026
Design note from the #6671 service-reliability pass, leaving this open.
I looked at whether a pidfd binding could land alongside the endpoint hardening (#7027, carried in its replacement PR). It is not a small change, so it stays out of that bundle:
bun:ffiinto libc.pidfd_openandpidfd_send_signalhave glibc wrappers only from 2.36, and musl has none. The fallback is the variadicsyscall(), which is awkward through FFI. The syscall numbers (434/424) are shared by x86_64 and arm64, but that still has to be pinned and tested per architecture.ppidcheck. EPERM, EMFILE, or ESRCH during acquisition must refuse to arm. As noted above, an unexpected bind failure must never fall back to a kill by PID.ppidchecked again afterpidfd_open. The descriptor has to be closed on every teardown, refusal, and EOF path. Injected syscall fixtures would need to cover parent exit or reparenting during acquisition, PID reuse, an already exited target, repeated recovery, and cancellation.For scale, the window this closes is the gap between the
process.ppidcheck andkill()insrc/service/health-sentinel-main.ts. In that gap the proxy would have to exit, be reaped by the service manager, and have its PID handed to a new process. It is a real residual, but it is narrow, and the feature is off by default. I think it fits a dedicated PR with the fixture set above better than a rider on #6671.Ingwannu commentedon Oct 16, 2026
Fixed on
devby #7133, commitb43f931465efc0c9128fdfd3d3f5568294bf91d6.Linux sentinel recovery now acquires and validates a pidfd before arming and signals through that fd. Bound signal failures never fall back to a numeric PID; unsupported startup cases retain the documented limited fallback.
I checked that the merge commit is an ancestor of current inspected
dev@4ae49360b7and that its implementation is present. Regression commands and results are recorded in #7133; I did not rerun those tests in this triage pass.Closing as completed on the integration branch. This is not a claim that the fix has reached an npm/desktop release; please use a build containing the commit when checking the result.