Skip to content

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

Open
rohanpoudel2 wants to merge 4 commits 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 4 commits 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
@mldangelo-oai

Copy link
Copy Markdown
Collaborator

@codex review exact head 8f0bc4d

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f0bc4d3a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread sdk/typescript/src/runtime.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 8f0bc4d3a3

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

`process.kill` narrows its pid argument to a 32-bit signed integer and
throws ERR_INVALID_ARG_TYPE when the value does not survive that round
trip. `Number.isSafeInteger` still admits positive integers above that
range, such as 2147483648, so an aged `owner.json` naming one reached
`process.kill`, and the argument error - being neither ESRCH nor EPERM -
escaped `recoverStaleCredentialHomeLock` and failed the acquisition
outright instead of reclaiming the malformed lock.

Reject any owner pid above the maximum `process.kill` accepts so it is
treated as unidentifiable, letting the age check reclaim the lock, and
cover the gap between the pid range and the safe-integer range in the
stale-lock test.
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.

2 participants