fix(spm): eliminate TOCTOU races in cache prep and spm.sh cleanup (DEVA11Y-482,483)#29
Open
Crash0v3rrid3 wants to merge 1 commit into
Open
fix(spm): eliminate TOCTOU races in cache prep and spm.sh cleanup (DEVA11Y-482,483)#29Crash0v3rrid3 wants to merge 1 commit into
Crash0v3rrid3 wants to merge 1 commit into
Conversation
…VA11Y-482,483) Two confirmed time-of-check/time-of-use race conditions (APPSEC-415): F-013 (DEVA11Y-482) — Cache version-dir TOCTOU in the SPM plugin: prepareArtifact() did a non-atomic check -> removeItem -> createDirectory -> extract on the cache <version> directory. Concurrent plugin invocations could corrupt each other's binary state. The download/extract now happens in a per-invocation staging dir (<version>.tmp.<UUID>) and is atomically published into place via FileManager.moveItem. If the move fails because another invocation already published the directory, the loser falls back to reading the existing executable. The non-concurrent happy path is unchanged; forceDownload swaps the existing dir aside atomically before publishing. F-014 (DEVA11Y-483) — Concurrent spm.sh share CWD; cleanup trap deletes a sibling's Package.swift: the synthetic manifest was written into the user's CWD and the EXIT trap ran `rm -f Package.swift Package.resolved`, so the first instance to exit deleted the manifest out from under a second concurrent instance. The synthetic manifest is now written into a per-invocation `mktemp -d` dir, passed to `swift package` via `--package-path`, and the cleanup trap removes only that temp dir (never files in the user's CWD). Include globs are rooted at the original working directory so the scan still targets the user's sources. Applied identically to the bash, zsh, and fish variants. The pre-existing user-supplied-Package.swift path is unchanged. Scoped to the cleanup-trap / temp-dir CWD fix only; the self-update and dependency-pinning changes (DEVA11Y-475/477/478) to the same spm.sh files are handled in a separate PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two confirmed time-of-check/time-of-use (TOCTOU) race conditions in the Swift SPM plugin and its launcher scripts. Umbrella: APPSEC-415.
F-013 — Cache version-dir TOCTOU (DEVA11Y-482, Medium)
Plugins/BrowserStackAccessibilityLint/BrowserStackAccessibilityLint.swift→prepareArtifact()Previously the cache
<version>directory was prepared non-atomically:fileExists→removeItem→createDirectory→ extract. Two concurrent plugin invocations (e.g. parallel Xcode build phases) could delete/recreate the directory under each other, corrupting the extracted binary.Fix: download + extract into a per-invocation staging dir
<version>.tmp.<UUID>, normalize the binary inside it, then atomically publish viaFileManager.moveIteminto the final version directory.moveItemthrows if the destination already exists → the losing invocation falls back to reading the already-published executable. The non-concurrent happy path is unchanged;forceDownloadswaps any existing directory aside atomically before publishing. Adefercleans up the staging dir on any error path.F-014 — Concurrent
spm.shshare CWD; cleanup trap deletes siblingPackage.swift(DEVA11Y-483, Medium)scripts/bash/spm.sh,scripts/zsh/spm.sh,scripts/fish/spm.shWhen the user has no
Package.swift, the script wrote a synthetic manifest into the current working directory and theEXITtrap ranrm -f Package.swift Package.resolved. Two concurrent invocations in the same CWD: the first to exit deleted the manifest out from under the second.Fix: write the synthetic manifest into a per-invocation
mktemp -ddirectory and pass it toswift packagevia--package-path. The cleanup trap now removes only that temp dir — never any file in the user's CWD. Include globs are rooted at the original working directory ($PWD) so the scan still targets the user's sources. The pre-existing path where the user already has aPackage.swiftis unchanged (scanned in place as before). Applied identically across all three shell variants.Validation
bash -nclean on all three scripts.shellcheck -s bashon all three: only pre-existing warnings remain (shebangSC2096,$?-after-testSC2319,EXTRA_ARGS=$@SC2124, echo-redirect styleSC2129, word-splittingSC2086). No new findings introduced; the existing intentional word-splitting of$EXTRA_ARGSis what carries the (now absolute) include globs to the CLI.swiftc -parseclean on the plugin file. Fullswift buildis N/A — the package is plugin-only (no buildable target), so the plugin source only compiles inside the SPM plugin sandbox.Caveats / follow-ups
$PWD(e.g.$PWD/**/*.swift) instead of CWD-relative, because the plugin's package directory is now the temp dir rather than the user's CWD. The downloadedbrowserstack-cli a11y scanmust accept absolute include globs. The CLI source lives inaccessibility-devtools-cli(not this repo), so this was not exercised end-to-end here — please verify a real scan still matches files before merge.--force-downloadpath swaps an existing version dir aside before publishing; concurrent force-downloads have a small non-atomic window. Normal (non-force) concurrent invocations are fully atomic. Acceptable since force-download is an explicit refresh operation.Overlap note
A sibling change (DEVA11Y-475/477/478: self-update + dependency pinning) edits the same
spm.shfiles. This PR is deliberately scoped to only the cleanup-trap / temp-dir CWD fix so the two PRs are independently reviewable; thescript_self_updatefunction and dependency URL are untouched here.Jira: DEVA11Y-482, DEVA11Y-483 · Umbrella: APPSEC-415
🤖 Generated with Claude Code