Skip to content

fix(runtime): reclaim credential-home locks whose owner names no process - #229

Open
rohanpoudel2 wants to merge 1 commit into
openai:mainfrom
rohanpoudel2:fix/lock-pid-validation
Open

fix(runtime): reclaim credential-home locks whose owner names no process#229
rohanpoudel2 wants to merge 1 commit into
openai:mainfrom
rohanpoudel2:fix/lock-pid-validation

Conversation

@rohanpoudel2

Copy link
Copy Markdown

Refs #228

Problem

recoverStaleCredentialHomeLock consulted process.kill(pid, 0) for any owner.json whose pid was a number:

if (isRecord(owner) && typeof owner["pid"] === "number") {
  try {
    process.kill(owner["pid"], 0);
    return false;

POSIX gives two of those numbers special meanings — 0 signals the caller's own process group and -1 every process it may signal — so both always succeed and always report a live owner. A fractional or out-of-range value makes process.kill throw ERR_INVALID_ARG_TYPE, which is neither ESRCH nor EPERM and was rethrown raw out of a public API.

The age check sits in the else branch, so a lock naming any of these values was also exempt from it. acquireCodexSecurityCredentialHomeLock therefore waited on such a lock forever, at a 25 ms poll, with no message and no timeout — while every other stale path (missing or corrupt owner.json) has a 30 s escape hatch.

Measured against the public API, with a 1.5 s abort as the only way out:

dead pid, 1h old (control)                     ACQUIRED in 2ms
no owner.json, 1h old (control)                ACQUIRED in 1ms
pid 0 -> process group, 24h old                ABORT_ERR: hung 1500ms
pid -1 -> every process, 24h old               ABORT_ERR: hung 1502ms
pid 0.5 -> ERR_INVALID_ARG_TYPE                ERR_INVALID_ARG_TYPE: hung 1ms

Change

Consult process.kill only for a positive safe integer. Anything else is an owner that cannot be identified, and is now treated exactly like a missing one, so the existing 30 s age check reclaims the lock. After the change the same three cases acquire in 2 ms.

The EPERM handling is unchanged and still means "the process exists but we may not signal it".

Scope: what this deliberately does not fix

A genuinely reused pid is still indistinguishable from the original owner, so a lock left behind by a SIGKILLed scan is still never reclaimed. I left that out on purpose:

#228 documents that part, including why it is close to guaranteed in the shipped container (Dockerfile puts the state dir under the /output bind mount, and compose.yaml sets init: true, so container pids restart from 1 each run and a leftover low pid collides). Happy to implement the heartbeat if you tell me which shape you want.

Verification

New test recovers credential-home locks whose owner names no process walks [0, -1, 0.5, 2 ** 53], ages the lock past the 30 s threshold, and bounds the acquisition with an AbortController so a regression fails the test in 5 s instead of hanging it.

Before: AbortError: The operation was aborted.0 pass / 1 fail in 5002 ms.
After: 1 pass / 0 fail in 54 ms.

Full suite: 718 pass / 5 skip / 0 fail (717 baseline plus the new test). pnpm run types and pnpm run format are clean.

`recoverStaleCredentialHomeLock` consulted `process.kill(pid, 0)` for any
`owner.json` whose `pid` was a number. POSIX gives two of those numbers
special meanings: 0 signals the caller's own process group and -1 every
process it may signal, so both always succeed and always report a live
owner. A fractional or out-of-range value makes `process.kill` throw
`ERR_INVALID_ARG_TYPE`, which is neither ESRCH nor EPERM and was rethrown
raw out of a public API.

Because the age check sits in the `else` branch, a lock naming any of these
values was also exempt from it, so `acquireCodexSecurityCredentialHomeLock`
waited on it forever at a 25 ms poll with no message and no timeout.

Only consult `process.kill` for a positive safe integer. Anything else is an
owner that cannot be identified, and is now treated like a missing one, so
the existing 30 s age check reclaims the lock.

This does not address a genuinely reused pid, which needs a heartbeat rather
than a liveness probe and is a larger design change. That part is described
in the issue.

Refs openai#228
@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant