fix(cache): atomic version-dir publish to close cache TOCTOU (DEVA11Y-482) - #32
fix(cache): atomic version-dir publish to close cache TOCTOU (DEVA11Y-482)#32Crash0v3rrid3 wants to merge 3 commits into
Conversation
…-482) Extract the CLI into a unique staging directory and atomically rename it into place instead of the check-delete-recreate sequence. Concurrent SPM builds sharing ~/.cache can no longer wipe each other's in-progress extraction; a build that loses the publish race reuses the winner's fully-formed binary rather than running a partially-written one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…A11Y-482) Review of PR #32 found a regression and residual races in the atomic-publish change: - P1: locateExecutable recurses, so a binary with the right name can sit in a nested subdir; the old lastPathComponent check skipped relocating it, leaving stagedExecutableURL/expectedExecutableURL pointing at a non-existent top-level path (ensureExecutablePermissions would throw). Compare full standardized URLs so any not-already-in-place binary is relocated. - P2: the publish catch did a non-atomic fileExists->removeItem->moveItem that could ENOENT-crash or fail a build when a peer republished concurrently. Make removeItem best-effort and, on retry failure, reuse a peer's valid binary instead of throwing. - P3: reword the publishVersionDirectory doc to stop overstating atomicity of the replace path; use UUID().uuidString for the staging suffix to match the file's existing convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the PR #32 review, closing the report-only findings: - Staging leak (#4): extraction helpers exit() via forwardExit on failure and SIGKILL bypasses defer, leaking .tmp.* staging dirs. Add sweepStaleStaging(), a best-effort mtime-gated sweep (>1h old only, so a concurrent build's in-flight staging is never deleted) run at the start of prepareArtifact. - Cross-platform moveItem (#5): stop depending on moveItem's throw-on-existing semantics (Darwin throws; POSIX rename silently replaces an empty dir). Check versionDirectory existence explicitly; absent -> atomic rename, present -> reuse/replace. Removes the untestable platform assumption. - Windows archive (#6): download the .zip to a sibling temp file outside the staging dir (with its own defer cleanup) so a failed removal can never bake the archive into the published version directory; a leftover is swept later. Not addressed here: binary integrity/signature verification (#7) is pre-existing, needs a trusted out-of-band digest source, and belongs with APPSEC-415 — not this TOCTOU fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code PR ReviewPR: #32 • Head: 833eff8 • Reviewers: stack:pr-review (aggregated correctness/security/testing/perf/quality) SummaryFixes a cache-directory TOCTOU (DEVA11Y-482): the SPM plugin now extracts the CLI into a unique staging directory and atomically renames it into the final version directory ( Review Table
Findings
Residual (non-gating): the Verdict: PASS — closes the TOCTOU correctly; no High/Critical. Recommend the Medium test-coverage gap as follow-up. |
What & why
Fixes DEVA11Y-482 (F-013, CWE-362, Medium) — a check‑delete‑recreate TOCTOU in the SPM plugin's CLI cache.
prepareArtifactdidfileExists → removeItem → createDirectory → extracton the shared~/.cacheversion directory. Two concurrent SPM builds (Xcode parallel targets, or a CI matrix sharing a cache volume) can both miss theisExecutableFilefast‑path and enter this path; one instance'sremoveItem/createDirectorythen wipes the other's in‑progress extraction. Result: non‑deterministic build failures, orlocateExecutable's fallback running a partially‑written (or planted) binary.Change
.tmp.<version>.<globallyUniqueString>) under the cache root, normalise the binary name and set permissions inside staging.FileManager.moveItem(rename(2)) so a version directory only ever becomes visible fully‑formed.publishVersionDirectory(...)helper: if the destination already exists (another build won, orforceDownloadis replacing a stale copy) reuse the valid published binary, otherwise replace and retry the rename once.defercleans up the staging directory on every exit path.Testing
swiftc -parseclean; brace balance verified. The package vends only a plugin (no buildable target), so full compilation happens via a consumer — not run here. Uses only Foundation APIs already present in the file (moveItem,removeItem,createDirectory,ProcessInfo.processInfo.globallyUniqueString).Draft for review — please sanity‑check the
forceDownloadconcurrent‑refresh branch.🤖 Generated with Claude Code