Skip to content

Fix daemon registry ownership validation and remove unused function

829b538
Select commit
Loading
Failed to load commit list.
Sign in for the full log view
Merged

feat: Workspace filesystem cleanup #391

Fix daemon registry ownership validation and remove unused function
829b538
Select commit
Loading
Failed to load commit list.
GitHub Actions / warden: find-bugs completed May 4, 2026 in 12m 47s

5 issues

find-bugs: Found 5 issues (3 medium, 2 low)

Medium

forceStopDaemon may leave socket and registry in place when process hasn't exited within 500ms - `src/cli/daemon-control.ts:53-57`

forceStopDaemon sends SIGTERM, waits only 500ms, then calls cleanupWorkspaceDaemonFiles without allowLiveOwner: true. In daemon-registry.ts, canRemoveRegistryEntry requires !isPidAlive(entry.pid) when allowLiveOwner is not set, so if the daemon hasn't died yet the registry entry and socket file are not removed. Since forceStopDaemon's documented purpose is to remove the stale socket after a version-mismatch force-stop, callers (e.g. ensureDaemonRunning) may immediately try to start a new daemon while the old socket is still present, defeating the cleanup.

Also found at:

  • src/daemon.ts:334-351
ensureSocketDir TOCTOU lets a pre-existing directory bypass the 0o700 mode - `src/daemon/socket-path.ts:29-36`

ensureSocketDir() guards mkdirSync with existsSync(dir); if the directory already exists (e.g., created by another local user in the shared tmpdir before the daemon starts) the function returns without enforcing mode 0o700 or verifying ownership. Combined with the move of the socket directory under os.tmpdir() in daemonDirForWorkspaceKey(), a local attacker can pre-create the per-workspace directory world-readable/writable and the daemon will happily place its UNIX socket and registry inside it, exposing IPC traffic.

runningScheduledSweeps lock leaks when sweep promise is never created - `src/utils/workspace-filesystem-lifecycle.ts:471-488`

In scheduleWorkspaceFilesystemLifecycleSweep, runningScheduledSweeps.add(scheduleKey) is called synchronously, but the corresponding delete only fires inside the setTimeout's .finally(). If the timer is cancelled (e.g. process shutdown before delay elapses) or if runWorkspaceFilesystemLifecycleSweep throws synchronously before returning a promise, the scheduleKey is never removed and all future scheduling for that workspace/logDir is silently blocked for the lifetime of the process.

Also found at:

  • src/utils/fs-lock.ts:138-148

Low

Successful recovery on second attempt is discarded without retrying createLock - `src/utils/fs-lock.ts:209-228`

In tryAcquireFsLock the retry loop runs only attempts 0 and 1. If attempt=1 hits EEXIST and tryRecoverExpiredLockDir succeeds, the loop ends (attempt becomes 2) without performing the createLock that the recovery enabled. The function then falls through to return null, so a recoverable lock is reported as unacquirable and callers must retry, defeating the purpose of the second attempt.

Also found at:

  • src/utils/fs-lock.ts:76-95
buildSchedulePreKey cooldown can be bypassed by alternating workspaceKey and logDir - `src/utils/workspace-filesystem-lifecycle.ts:446-454`

buildSchedulePreKey returns either a workspace: or logDir: key based on which option is present. A caller that schedules first with {workspaceKey} and then with {logDir} (pointing to the same workspace's logs) will produce different preKeys, defeating the cooldown. Combined with the separate scheduleKey (workspaceKey:logDir), this allows scheduling far more sweeps than intended for the same underlying directory.

Also found at:

  • src/utils/workspace-filesystem-lifecycle.ts:492-521

Duration: 12m 40s · Tokens: 1.2M in / 20.0k out · Cost: $4.37 (+merge: $0.00)

Annotations

Check warning on line 57 in src/cli/daemon-control.ts

See this annotation in the file changed.

@github-actions github-actions / warden: find-bugs

forceStopDaemon may leave socket and registry in place when process hasn't exited within 500ms

forceStopDaemon sends SIGTERM, waits only 500ms, then calls cleanupWorkspaceDaemonFiles without allowLiveOwner: true. In daemon-registry.ts, canRemoveRegistryEntry requires !isPidAlive(entry.pid) when allowLiveOwner is not set, so if the daemon hasn't died yet the registry entry and socket file are not removed. Since forceStopDaemon's documented purpose is to remove the stale socket after a version-mismatch force-stop, callers (e.g. ensureDaemonRunning) may immediately try to start a new daemon while the old socket is still present, defeating the cleanup.

Check warning on line 351 in src/daemon.ts

See this annotation in the file changed.

@github-actions github-actions / warden: find-bugs

[JYJ-UYW] forceStopDaemon may leave socket and registry in place when process hasn't exited within 500ms (additional location)

forceStopDaemon sends SIGTERM, waits only 500ms, then calls cleanupWorkspaceDaemonFiles without allowLiveOwner: true. In daemon-registry.ts, canRemoveRegistryEntry requires !isPidAlive(entry.pid) when allowLiveOwner is not set, so if the daemon hasn't died yet the registry entry and socket file are not removed. Since forceStopDaemon's documented purpose is to remove the stale socket after a version-mismatch force-stop, callers (e.g. ensureDaemonRunning) may immediately try to start a new daemon while the old socket is still present, defeating the cleanup.

Check warning on line 36 in src/daemon/socket-path.ts

See this annotation in the file changed.

@github-actions github-actions / warden: find-bugs

ensureSocketDir TOCTOU lets a pre-existing directory bypass the 0o700 mode

ensureSocketDir() guards mkdirSync with existsSync(dir); if the directory already exists (e.g., created by another local user in the shared tmpdir before the daemon starts) the function returns without enforcing mode 0o700 or verifying ownership. Combined with the move of the socket directory under os.tmpdir() in daemonDirForWorkspaceKey(), a local attacker can pre-create the per-workspace directory world-readable/writable and the daemon will happily place its UNIX socket and registry inside it, exposing IPC traffic.

Check warning on line 488 in src/utils/workspace-filesystem-lifecycle.ts

See this annotation in the file changed.

@github-actions github-actions / warden: find-bugs

runningScheduledSweeps lock leaks when sweep promise is never created

In scheduleWorkspaceFilesystemLifecycleSweep, runningScheduledSweeps.add(scheduleKey) is called synchronously, but the corresponding delete only fires inside the setTimeout's .finally(). If the timer is cancelled (e.g. process shutdown before delay elapses) or if runWorkspaceFilesystemLifecycleSweep throws synchronously before returning a promise, the scheduleKey is never removed and all future scheduling for that workspace/logDir is silently blocked for the lifetime of the process.

Check warning on line 148 in src/utils/fs-lock.ts

See this annotation in the file changed.

@github-actions github-actions / warden: find-bugs

[JPM-S8N] runningScheduledSweeps lock leaks when sweep promise is never created (additional location)

In scheduleWorkspaceFilesystemLifecycleSweep, runningScheduledSweeps.add(scheduleKey) is called synchronously, but the corresponding delete only fires inside the setTimeout's .finally(). If the timer is cancelled (e.g. process shutdown before delay elapses) or if runWorkspaceFilesystemLifecycleSweep throws synchronously before returning a promise, the scheduleKey is never removed and all future scheduling for that workspace/logDir is silently blocked for the lifetime of the process.