Skip to content

sandbox: isolate /tmp per instance (fixes #82) - #87

Merged
odrobnik merged 2 commits into
mainfrom
sandbox-per-instance-tmp
Jun 11, 2026
Merged

sandbox: isolate /tmp per instance (fixes #82)#87
odrobnik merged 2 commits into
mainfrom
sandbox-per-instance-tmp

Conversation

@odrobnik

Copy link
Copy Markdown
Contributor

Summary

In --sandbox mode, virtual /tmp was mounted onto the bare NSTemporaryDirectory() root — one namespace shared by every SwiftBash instance in the process and by everything else using the platform temp dir (#82). Each sandbox now gets its own swiftbash-<UUID> dir under the platform temp root, removed when the run ends.

This is the minimal fix ahead of the #83 refactoring (mapping + confinement core in ShellKit); nothing here gets thrown away by it — #83 retires the identity mount and what's left of the carve-out, then flips $TMPDIR to the virtual /tmp spelling.

What changed

  • ExecCommand.makeSandboxFileSystem creates the per-instance dir, mounts it at virtual /tmp, and identity-mounts it at its own host path so $TMPDIR/foo and /tmp/foo reach the same files. On Linux the identity mount nests inside the /tmp mount; longest-virtual-prefix routing keeps it from double-nesting (regression-tested).
  • $TMPDIR carries the instance dir's host path — not virtual /tmp yet, because FileManager-backed callers (SwiftPorts CLIs, SwiftScript, JS) have no virtual→host translation until Sandbox path enforcement: move the mapping into ShellKit and offer two facades over one core #83. The CLI defer-removes the dir however the script ends.
  • Sandbox.bashWorkspace derives the URL gate's temp carve-out from the passed temporaryDirectory (normalized + symlink-resolved spellings) instead of a process-wide static let. The hardcoded /tmp, /private/tmp, and bare-temp-root prefixes are gone — a sibling instance's dir or the shared root no longer authorizes, so isolation is enforced for bridge callers too, not just bash builtins. SwiftJSCore's os.tmpdir() picks up the per-instance dir automatically via sandbox.temporaryDirectory.
  • Tests: gate tests rewritten around explicit instance dirs (literal /tmp now expects denial; new sibling-instance and shared-root denial coverage); new tmpIsIsolatedPerInstance (two sandboxes write the same hardcoded /tmp/secret.txt without cross-talk) and tmpSpellingsReachTheSameFiles (the Linux nesting guard).
  • Docs: Sandboxing.md mount layout, embedder sample, and the threat-model paragraph that previously documented the shared /tmp as a tradeoff; README.md; AGENTS.md.

Interim tradeoff (until #83)

Ports/JS handed a literal /tmp/... path are denied. That spelling only ever worked on Linux by coincidence (NSTemporaryDirectory() is /tmp there) and silently diverged to /private/tmp on macOS — failing loudly beats reading the shared root. $TMPDIR-spelled access — the only spelling that worked on every platform — keeps working everywhere, and bash builtins are unaffected (/tmp translates through the mount table as before). Documented in Docs/Sandboxing.md. #83's resolve() translation restores the literal spelling for ports properly.

Verification

  • Full suite: 1987 tests, only the 3 known timing-sensitive pipeline tests flake under parallel load (reproduced identically on clean main; pass in isolation). SwiftLint strict: clean.
  • End-to-end with the built binary: $TMPDIR points at the per-instance dir; /tmp writes land there and not in the shared root; mount output leaks no host path; mktemp works; jq reads bash-written files via $TMPDIR spelling; jq on a literal /tmp/... path is denied; zero swiftbash-* dirs remain after runs exit.

Fixes #82.

🤖 Generated with Claude Code

Virtual /tmp in --sandbox mode previously mounted the bare
NSTemporaryDirectory() root, so every SwiftBash instance — and every
other process using the platform temp dir — shared one namespace:
cross-instance reads, predictable-name collisions, and a TOCTOU
surface, as reported in #82.

Each sandbox now gets its own swiftbash-<UUID> dir under the platform
temp root:

- ExecCommand.makeSandboxFileSystem creates the dir, mounts it at
  virtual /tmp, and identity-mounts it at its own host path so
  $TMPDIR-spelled and /tmp-spelled paths reach the same files. On
  Linux the identity mount nests inside the /tmp mount;
  longest-virtual-prefix routing keeps it from double-nesting.
- $TMPDIR carries the instance dir's host path (not virtual /tmp yet:
  FileManager-backed callers have no virtual->host translation until
  the #83 mapping core lands in ShellKit).
- The CLI removes the dir however the run ends.
- Sandbox.bashWorkspace derives the URL gate's temp carve-out from
  the passed temporaryDirectory (normalized + symlink-resolved
  spellings) instead of a process-wide static list. The hardcoded
  /tmp, /private/tmp, and bare-root prefixes are gone, so a sibling
  instance's dir or the shared root no longer authorizes.

Interim tradeoff, documented in Docs/Sandboxing.md: ports/JS handed a
literal /tmp/... path are denied until #83 — that spelling only ever
worked on Linux by coincidence (on macOS it silently diverged to
/private/tmp). $TMPDIR-spelled access keeps working on every
platform, and bash builtins are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb8e97b341

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/BashInterpreter/API/Sandbox+BashWorkspace.swift
The carve-out derived its accepted prefixes from standardizingPath +
resolvingSymlinksInPath only. standardizingPath can rewrite the
spelling (corelibs-foundation follows symlinks; Darwin strips
/private), and the gate checks the unresolved path first — so on a
host where the temp dir sits behind a symlink, the one spelling
callers actually use ($TMPDIR as the embedder set it) could vanish
from the list and deny legitimate access. Keep tempDir.path verbatim
alongside the other two; all three name the same directory.

Review feedback on #87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@odrobnik
odrobnik merged commit 6ffaee1 into main Jun 11, 2026
6 checks passed
@odrobnik
odrobnik deleted the sandbox-per-instance-tmp branch June 11, 2026 09:38
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.

Sandbox /tmp is shared across all instances (and the host) — isolate it per sandbox

1 participant