Skip to content

Version-replacement handoff is not graceful: SIGTERM then a 5ms busy-wait before respawn, so a sidecar holding an exclusive resource fails to start with no retry #2280

Description

@kriszyp

Location: security/jsLoader.ts:1017-1032 (version-replacement branch in acquirePidFileLock)

Impact

The options.version handoff sends SIGTERM and then starts the replacement ~5ms later without waiting for the old process to exit. A sidecar that holds a listening socket, a lock file, or any other exclusive resource can fail on startup — EADDRINUSE is the obvious case — and there is no retry: the caller gets a child that dies immediately.

Details

if (requestedVersion != null && requestedVersion !== existingVersion) {
    try { process.kill(existingPid); } catch { }      // SIGTERM, no wait
    try { unlinkSync(pidFilePath); } catch { }
    const start = Date.now();
    while (Date.now() - start < retryDelay) {          // busy-wait, retryDelay = 5ms
    }
    continue;                                          // -> lock acquired -> spawn replacement
}

retryDelay defaults to 5. There is no waitpid, no poll on isProcessRunning(existingPid) (which the module already has, at security/jsLoader.ts:970), and no readiness or retry on the spawned replacement. The busy-wait also burns the thread rather than yielding.

5ms is not enough for a process to run its SIGTERM handler, flush, and close a listening socket. For a sidecar that ignores SIGTERM entirely it is never enough.

Reproduction

--- CLAIM 3: is the outgoing process still alive when the replacement spawns? ---
  old pid 59797 SIGTERMed; new pid 59806 spawned 5.6ms later
  old process still alive at the instant the replacement started? true
  (no waitpid / no readiness check / no retry between the two)

Recommended fix

Poll isProcessRunning(existingPid) with a bounded deadline before acquiring the lock, escalating SIGTERM → SIGKILL if the deadline passes, and yield rather than busy-wait. The module already has the liveness primitive; the handoff just doesn't use it.

Related: harper#2076 is the same class of defect (termination confirmation) in the component install/deploy spawn path in components/Application.ts, but with the opposite failure — an unbounded poll. A shared, bounded "wait for confirmed termination" helper would serve both.

Priority note

Filed P3 deliberately: real, but nobody will schedule it on its own. It should be picked up by whoever rewrites this handoff for the double-process defect in harper#2279, which is why they are cross-linked.

Affected versions

All v5 lines carrying options.version support. Confirmed on origin/main @ f8a5aa90a (v5.2.4).


Filed by KrAIs (Claude Opus 5). Found while documenting this module for HarperFast/documentation#634.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    P3

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions