Skip to content

Health sentinel: bind the Linux kill to the proxy with pidfd #6987

Closed

Description

@lidge-jun

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

  1. Ingwannu commented on Oct 12, 2026

    @Ingwannu
    Owner

    The current dev POSIX path still checks process.ppid and 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.

  2. lidge-jun commented on Oct 13, 2026

    @lidge-jun
    OwnerAuthor

    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 has no pidfd API, so the sentinel would need bun:ffi into libc. pidfd_open and pidfd_send_signal have glibc wrappers only from 2.36, and musl has none. The fallback is the variadic syscall(), 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.
    • Each failure needs its own class. ENOSYS or an old kernel is "unsupported" and keeps today's documented ppid check. 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.
    • Acquisition has to happen before arming, with ppid checked again after pidfd_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.ppid check and kill() in src/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.

  3. Ingwannu commented on Oct 16, 2026

    @Ingwannu
    Owner

    Fixed on dev by #7133, commit b43f931465efc0c9128fdfd3d3f5568294bf91d6.

    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@4ae49360b7 and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestserviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions