feat: Workspace filesystem cleanup #391
3 issues
find-bugs: Found 3 issues (2 medium, 1 low)
Medium
Daemon socket directory in shared tmpdir is predictable and vulnerable to local pre-creation/symlink attacks - `src/daemon/socket-path.ts:29-36`
daemonDirForWorkspaceKey now constructs <tmpdir>/xcodebuildmcp-<12-hex-of-sha256(workspaceRoot)>. On Linux/macOS tmpdir() is typically /tmp, a world-writable directory shared across local users. The 12-hex suffix is fully deterministic from the (often guessable) workspace root path, so a co-resident local user can pre-create the directory (or a symlink at that name) before the victim's daemon starts. ensureSocketDir calls mkdirSync(dir, { recursive: true, mode: 0o700 }), but Node's mkdirSync does not modify mode on an existing directory, so the 0o700 protection is silently bypassed when the path already exists. Combined with removeStaleSocket blindly unlinkSync-ing inside that directory, a local attacker can squat on the path, intercept/disrupt the daemon's IPC socket, or force the daemon to bind into an attacker-controlled location. The previous implementation placed these under ~/.xcodebuildmcp/daemons/<key>, which is inside the user's home directory and not exposed to other local users.
isPidAlive treats EPERM as alive but ignores invalid pid 0/negative which targets process group - `src/utils/process-liveness.ts:1-8`
process.kill(pid, 0) with pid <= 0 has special semantics in POSIX: pid 0 signals every process in the caller's process group, and negative pids signal a process group. Passing such values to isPidAlive will not check liveness of a specific process and may return true based on group membership rather than the intended target. Callers using this for live-owner detection during cleanup could incorrectly conclude that a stale/invalid recorded pid (e.g., 0) is still alive and skip cleanup of artifacts indefinitely.
Low
removeDaemonRegistryEntry and cleanupWorkspaceDaemonFiles silently swallow lock-acquisition failures - `src/daemon/daemon-registry.ts:286-345`
withDaemonRegistryMutationLock returns null when the bounded busy-wait for the daemon registry lock times out (DAEMON_REGISTRY_LOCK_WAIT_MS = 1s). writeDaemonRegistryEntry checks for this null and throws, but removeDaemonRegistryEntry (lines 286-288) and cleanupWorkspaceDaemonFiles (lines 331-345) discard the return value. Under contention from another MCP server, daemon, or CLI, cleanup will silently no-op, leaving stale registry/socket files behind that can mislead subsequent daemon startup or liveness checks.
Duration: 15m 52s · Tokens: 1.3M in / 18.5k out · Cost: $4.48 (+extraction: $0.00, +merge: $0.00)
Annotations
Check warning on line 36 in src/daemon/socket-path.ts
sentry-warden / warden: find-bugs
Daemon socket directory in shared tmpdir is predictable and vulnerable to local pre-creation/symlink attacks
`daemonDirForWorkspaceKey` now constructs `<tmpdir>/xcodebuildmcp-<12-hex-of-sha256(workspaceRoot)>`. On Linux/macOS `tmpdir()` is typically `/tmp`, a world-writable directory shared across local users. The 12-hex suffix is fully deterministic from the (often guessable) workspace root path, so a co-resident local user can pre-create the directory (or a symlink at that name) before the victim's daemon starts. `ensureSocketDir` calls `mkdirSync(dir, { recursive: true, mode: 0o700 })`, but Node's `mkdirSync` does not modify mode on an existing directory, so the 0o700 protection is silently bypassed when the path already exists. Combined with `removeStaleSocket` blindly `unlinkSync`-ing inside that directory, a local attacker can squat on the path, intercept/disrupt the daemon's IPC socket, or force the daemon to bind into an attacker-controlled location. The previous implementation placed these under `~/.xcodebuildmcp/daemons/<key>`, which is inside the user's home directory and not exposed to other local users.
Check warning on line 8 in src/utils/process-liveness.ts
sentry-warden / warden: find-bugs
isPidAlive treats EPERM as alive but ignores invalid pid 0/negative which targets process group
process.kill(pid, 0) with pid <= 0 has special semantics in POSIX: pid 0 signals every process in the caller's process group, and negative pids signal a process group. Passing such values to isPidAlive will not check liveness of a specific process and may return true based on group membership rather than the intended target. Callers using this for live-owner detection during cleanup could incorrectly conclude that a stale/invalid recorded pid (e.g., 0) is still alive and skip cleanup of artifacts indefinitely.