Skip to content

feat: Workspace filesystem cleanup

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

feat: Workspace filesystem cleanup #391

feat: Workspace filesystem cleanup
87c486d
Select commit
Loading
Failed to load commit list.
GitHub Actions / warden: find-bugs completed May 4, 2026 in 12m 57s

4 issues

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

Medium

Daemon socket directory created in world-writable /tmp without permission verification - `src/daemon/socket-path.ts:21-23`

The daemon directory is now placed under tmpdir() (typically /tmp, world-writable with sticky bit) using a predictable path xcodebuildmcp-<12-hex-chars>. Combined with ensureSocketDir's existsSync-then-mkdirSync pattern (in this file at lines 77-82), an attacker on a multi-user system could pre-create the directory or a symlink at the predictable path before the daemon starts, bypassing the intended mode: 0o700 and exposing the Unix domain socket to other local users. The previous location under ~/.xcodebuildmcp/daemons/<key> was inside the user's home directory and not subject to this race.

isPidAlive treats EPERM as alive but ignores invalid PIDs (0, negative) - `src/utils/process-liveness.ts:1-8`

process.kill(0, 0) on POSIX sends signal 0 to every process in the caller's process group, and negative PIDs target a process group rather than a single process. Because isPidAlive does not validate the input, callers passing a stale/zero/negative PID (e.g. from a corrupted registry file) can inadvertently probe or signal unrelated processes, and the function will report 'alive' based on group membership rather than the intended PID. In a cleanup path that uses liveness to decide whether to delete another workspace's artifacts, this can both prevent legitimate cleanup and, if the signal value were ever changed, target unintended processes.

Low

compactWorkspaceKey collapses workspace keys to 12-hex chars enabling collisions across workspaces - `src/daemon/socket-path.ts:16-19`

compactWorkspaceKey extracts only the trailing 12-hex-character hash from the workspace key (or hashes the whole key to 12 hex chars) for the daemon directory name, while registryPathForWorkspaceKey and logPathForWorkspaceKey continue using the full workspace key (name + hash). Two workspaces whose name slugs differ but whose path hashes collide in 12 hex chars (~2^48 space) — or where a malicious workspace name is constructed to mimic the -<12hex>compactWorkspaceKeyextracts only the trailing 12-hex-character hash from the workspace key (or hashes the whole key to 12 hex chars) for the daemon directory name, whileregistryPathForWorkspaceKeyandlogPathForWorkspaceKey` continue using the full workspace key (name + hash). Two workspaces whose name slugs differ but whose path hashes collide in 12 hex chars (~2^48 space) — or where a malicious workspace name is constructed to mimic the suffix pattern of another — would share the same daemon directory and socket while having distinct registry/log directories, leading to socket cross-talk while state remains separate.

Scheduled sweep uses stale `now` captured at scheduling time - `src/utils/workspace-filesystem-lifecycle.ts:436-437`

In scheduleWorkspaceFilesystemLifecycleSweep, resolveOptions(options) is called synchronously and the resulting resolved (with resolved.now fixed at scheduling time) is captured in the setTimeout closure and passed to runWorkspaceFilesystemLifecycleSweep. When the timer fires (after at least WORKSPACE_FILESYSTEM_LIFECYCLE_SCHEDULE_DELAY_MS, possibly much longer under event-loop pressure), the sweep uses this stale now for cooldown checks (shouldSkipForCooldown), minVisibleMs protection in isProtectedLogFile, age expiration (options.now - file.mtimeMs > options.maxAgeMs), and the marker mtime written by touchCleanupMarker. This can cause the marker to be backdated and protection windows to use outdated time, producing slightly incorrect retention decisions and cooldown evaluations.


Duration: 12m 53s · Tokens: 1.2M in / 16.1k out · Cost: $6.35 (+extraction: $0.00, +merge: $0.00)

Annotations

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

See this annotation in the file changed.

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

Daemon socket directory created in world-writable /tmp without permission verification

The daemon directory is now placed under `tmpdir()` (typically `/tmp`, world-writable with sticky bit) using a predictable path `xcodebuildmcp-<12-hex-chars>`. Combined with `ensureSocketDir`'s `existsSync`-then-`mkdirSync` pattern (in this file at lines 77-82), an attacker on a multi-user system could pre-create the directory or a symlink at the predictable path before the daemon starts, bypassing the intended `mode: 0o700` and exposing the Unix domain socket to other local users. The previous location under `~/.xcodebuildmcp/daemons/<key>` was inside the user's home directory and not subject to this race.

Check warning on line 8 in src/utils/process-liveness.ts

See this annotation in the file changed.

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

isPidAlive treats EPERM as alive but ignores invalid PIDs (0, negative)

process.kill(0, 0) on POSIX sends signal 0 to every process in the caller's process group, and negative PIDs target a process group rather than a single process. Because isPidAlive does not validate the input, callers passing a stale/zero/negative PID (e.g. from a corrupted registry file) can inadvertently probe or signal unrelated processes, and the function will report 'alive' based on group membership rather than the intended PID. In a cleanup path that uses liveness to decide whether to delete another workspace's artifacts, this can both prevent legitimate cleanup and, if the signal value were ever changed, target unintended processes.