Skip to content

fix(daemon): do not let a compromised lockfile take the supervisor down - #1550

Closed
Jiaaqiliu wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Jiaaqiliu:fix/lockfile-compromise-crash
Closed

Jiaaqiliu wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Jiaaqiliu:fix/lockfile-compromise-crash

Conversation

@Jiaaqiliu

@Jiaaqiliu Jiaaqiliu commented Aug 19, 2026

Copy link
Copy Markdown

Problem

proper-lockfile defaults onCompromised to (err) => { throw err; } and calls it from setLockAsCompromised, which runs inside the lock's mtime-refresh filesystem callback. A throw there is an uncaught exception, not something the caller can handle.

None of the daemon's four lock sites pass the option:

  • modes/daemon/daemon-socket.ts (socket-path lease, and the sync cleanup lock)
  • modes/daemon/daemon-supervisor-ownership.ts (registry guard)
  • cli/daemon-update-restart.ts (coordinator guard)
  • core/session-lease.ts (lease guard)

core/auth-storage.ts already passes onCompromised, which is what the correct handling looks like.

Impact

The socket-path lease is the dangerous one. The supervisor acquires it at startup and holds it until it exits, while the refresh runs on a setTimeout that cannot fire while the event loop is blocked — and the supervisor blocks it routinely: execFileSync("ps") for process identity, Atomics.wait in the session-lease guard, readFileSync journal loads.

A stall past the 5s stale window lets a second supervisor declare the lock stale, rmdir it, and take over. The first supervisor's next refresh then throws ECOMPROMISED. daemon-supervisor.ts installs no uncaughtException handler (a repo-wide grep finds them only in daemon-mode.ts), so the process dies outright and every session's control plane goes with it.

Fix

Record the loss on the lease instead of throwing, and act on it where it actually matters:

  • prepareDaemonSocketPath fails startup loudly with a clear message
  • cleanupDaemonSocketPath skips the unlink, because the socket at that path may already belong to the successor that stole the lease

The other three sites get a non-throwing handler for the same reason: nothing may throw from that callback.

Tests

Adds packages/coding-agent/test/daemon-socket-lease-compromise.test.ts, which acquires a lease and then removes the lockfile to simulate a steal. On main the run reports unhandled errors from the refresh callback and both assertions fail.

All 33 existing daemon/socket/lease/supervisor suites (690 tests) still pass.

Note

Fix compromised lockfile events to prevent daemon supervisor crashes

  • Adds onCompromised: () => {} to proper-lockfile calls across daemon socket, session lease, coordinator registry, and supervisor registry guards so that lockfile compromise events no longer throw from filesystem callbacks.
  • Introduces a compromised state on DaemonSocketPathLease via markCompromised(error) and compromisedError getter, allowing callers to detect and react to stolen leases without crashing.
  • prepareDaemonSocketPath now throws a descriptive error when passed a compromised lease; cleanupDaemonSocketPath skips unlinking in the same case.
  • Adds integration tests in daemon-socket-lease-compromise.test.ts that simulate lock theft and verify correct behavior (skipped on Windows).

Macroscope summarized 4d310e8.

proper-lockfile defaults onCompromised to `(err) => { throw err; }` and calls
it from inside the lock's mtime-refresh filesystem callback, so the throw is
an uncaught exception rather than something the caller can handle. None of the
daemon's four lock sites passed the option; core/auth-storage.ts already does,
which is what the correct handling looks like.

The socket-path lease is the dangerous one. The supervisor acquires it at
startup and holds it until it exits, while the refresh runs on a timer that
cannot fire when the event loop is blocked - and the supervisor blocks it
routinely (execFileSync("ps") for process identity, Atomics.wait in the
session-lease guard, readFileSync journal loads). A stall past the 5s stale
window lets a second supervisor declare the lock stale, rmdir it, and take
over; the first supervisor's next refresh then throws. daemon-supervisor.ts
installs no uncaughtException handler, so the process dies and every session's
control plane goes with it.

Record the loss on the lease instead of throwing, and act on it where it
matters: prepareDaemonSocketPath fails startup loudly, and
cleanupDaemonSocketPath skips the unlink, because the socket at that path may
already belong to the successor that stole the lease.

The other three sites - the supervisor ownership registry guard, the
update-restart coordinator guard, and the session-lease guard - get a
non-throwing handler for the same reason.

Without this change the new tests report unhandled errors from the refresh
callback.
@github-actions

Copy link
Copy Markdown

Hi @Jiaaqiliu, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/PrimeIntellect-ai/prime-agent/blob/main/CONTRIBUTING.md for more details.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant