Skip to content

fix(spm): eliminate TOCTOU races in cache prep and spm.sh cleanup (DEVA11Y-482,483)#29

Open
Crash0v3rrid3 wants to merge 1 commit into
mainfrom
fix/DEVA11Y-482-toctou-race-fixes
Open

fix(spm): eliminate TOCTOU races in cache prep and spm.sh cleanup (DEVA11Y-482,483)#29
Crash0v3rrid3 wants to merge 1 commit into
mainfrom
fix/DEVA11Y-482-toctou-race-fixes

Conversation

@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator

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.swiftprepareArtifact()

Previously the cache <version> directory was prepared non-atomically: fileExistsremoveItemcreateDirectory → 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 via FileManager.moveItem into the final version directory. moveItem throws if the destination already exists → the losing invocation falls back to reading the already-published executable. The non-concurrent happy path is unchanged; forceDownload swaps any existing directory aside atomically before publishing. A defer cleans up the staging dir on any error path.

F-014 — Concurrent spm.sh share CWD; cleanup trap deletes sibling Package.swift (DEVA11Y-483, Medium)

scripts/bash/spm.sh, scripts/zsh/spm.sh, scripts/fish/spm.sh

When the user has no Package.swift, the script wrote a synthetic manifest into the current working directory and the EXIT trap ran rm -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 -d directory and pass it to swift package via --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 a Package.swift is unchanged (scanned in place as before). Applied identically across all three shell variants.

Validation

  • bash -n clean on all three scripts.
  • shellcheck -s bash on all three: only pre-existing warnings remain (shebang SC2096, $?-after-test SC2319, EXTRA_ARGS=$@ SC2124, echo-redirect style SC2129, word-splitting SC2086). No new findings introduced; the existing intentional word-splitting of $EXTRA_ARGS is what carries the (now absolute) include globs to the CLI.
  • swiftc -parse clean on the plugin file. Full swift build is N/A — the package is plugin-only (no buildable target), so the plugin source only compiles inside the SPM plugin sandbox.

Caveats / follow-ups

  • Absolute include globs (F-014): the scan now passes include patterns rooted at $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 downloaded browserstack-cli a11y scan must accept absolute include globs. The CLI source lives in accessibility-devtools-cli (not this repo), so this was not exercised end-to-end here — please verify a real scan still matches files before merge.
  • forceDownload concurrency (F-013): the explicit --force-download path 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.sh files. This PR is deliberately scoped to only the cleanup-trap / temp-dir CWD fix so the two PRs are independently reviewable; the script_self_update function and dependency URL are untouched here.

Jira: DEVA11Y-482, DEVA11Y-483 · Umbrella: APPSEC-415

🤖 Generated with Claude Code

…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>
@Crash0v3rrid3 Crash0v3rrid3 requested a review from a team as a code owner June 17, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant