Gate every Foundation IO door behind the host sandbox#4
Merged
Conversation
Closes #3. PR #2 routed IO/FS/network/identity/exit through `ShellKit.Shell.current` but the generator's `gates(...)` policy only inserted authorize-path / authorize-URL calls for FileManager path-arg methods, URLSession URL-arg methods, and the `String`/`Data` file-IO entry points. Every other Foundation IO door — `FileHandle(forReadingAtPath:)`, `Bundle(path:)`, `InputStream(url:)`, `FileWrapper(url:options:)`, `Process()` — still reached the host's real disk / process table even under a rooted Sandbox. This change closes the gaps listed in #3's "Known gaps" table. What changed: * New bridge receivers: `FileHandle`, `Bundle`, `InputStream`, `OutputStream`, `FileWrapper`, `Process`. The first five each get path/URL gating in `gates(...)` so a script can't open a file outside the sandbox via a class init. `Process` is denied entirely under any sandbox via a new `ProcessSandboxDenied` error from `denyProcessIfSandboxed()` — `Foundation.Process` spawns a real OS subprocess that escapes every host gate, and ShellKit's closure-based `ProcessTable` doesn't take a path-to-exec, so there's nothing to redirect to. * Generator: extend per-receiver rules with a generalised parameter-label scan. New `pathStringLabelsRead/Write` and `urlLabelsRead/Write` sets describe which `String`/`URL` argument labels carry filesystem paths; `scanByLabel` walks every parameter position and gates the matching ones (not just position 0). This closes the holes around two-path FileManager methods — `setUbiquitous(_:itemAt:destinationURL:)` (URL args at indices 1+2), `createSymbolicLink(at:withDestinationURL:)` (both URL args are writes), `contentsEqual(atPath:andPath:)` (the second path slipped past the gate). The receiver rule supplies a default intent; the label scan upgrades it (e.g. `to:` -> `.fsWrite` even when the receiver default is read). * Cross-references for the new types: `URLRequest.httpBodyStream: InputStream?`, `URLResource.bundle: Bundle`. * Host: reject unknown HTTP verbs in `NetworkConfig.checkAllowed`. Previously an unrecognised method silently fell back to `.GET`, which evaluated the request against GET permissions while sending the actual verb — a policy-bypass. * Tests: deny + allow paths in `ShellKitIntegrationTests` for each new receiver. Covers `FileHandle(forReadingAtPath:)`, `(forWritingAtPath:)`, `Bundle(path:)`, `(url:)`, `InputStream(fileAtPath:)`, `(url:)`, `OutputStream(toFileAtPath:)`, `(url:append:)`, `FileWrapper(url:options:)`, and `Process()` deny. Bridges regenerated via `Tools/regen-foundation-bridge.sh`. The mostly-reorder noise in `FoundationBridges.swift` and `StdlibBridges.swift` is generator-output non-determinism (dict iteration order); the only truly-new lines in `FoundationBridges.swift` are the gate-injected bodies for the new receivers (54 lines). Sorting the generator's dict output deterministically is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a6cb98958
ℹ️ 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".
`matchesContents(of:)` was bridged without an `authorizePath` check, letting a script call it with an out-of-root URL and still inspect host filesystem state — an FS sandbox bypass through the newly-added FileWrapper surface. The `of:` label isn't in `urlLabelsRead` (too generic to add globally — appears all over Foundation on non-URL args), so the prior `scanByLabel`-only branch missed it. Add a positional gate at index 0 of the FileWrapper receiver branch, mirroring how FileManager / URLSession treat their first arg. Test: `sandboxBlocksFileWrapperMatchesContentsOutsideRoot` constructs a FileWrapper(regularFileWithContents:) (no disk hop) under a rooted sandbox and asserts matchesContents(of:) on a path outside the root throws Sandbox.Denial. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Foundation.Process` is unavailable on the iOS family (iOS, tvOS, watchOS, visionOS). The scl oracle classified it cross-platform because Linux Foundation has it, so the generator emitted the Process bridge dict and comparator without any guard — the iOS build broke with "cannot find 'Process' in scope". Generator: introduce a third Platform case `nonIOSOnly` for types available on macOS / Linux / Windows / Android but not iOS-family. `nonIOSOnlyTypes = ["Process"]` overrides both the scl-oracle classification and the Apple-only fallback for class-typed comparators. The per-type bridge file gets wrapped in `#if !os(iOS) && !os(tvOS) && !os(watchOS) && !os(visionOS)` with a `[:]` stub on the excluded platforms so the manifest's reference still resolves; the runtime-body comparator uses the same guard. Tests: skip the two Process tests on the iOS family with a matching `#if`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Foundation.FileWrapper` isn't available on swift-corelibs-foundation (Linux / Windows / Android) — the bridge file is already `#if canImport(Darwin)`-only, so on those platforms the script fails to resolve `FileWrapper` and the test sees a "cannot find 'FileWrapper' in scope" error instead of `Sandbox.Denial`. Match the bridge's platform gating: wrap the new test in `#if canImport(Darwin)`. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Closes #3.
PR #2 routed IO/FS/network/identity/exit through
ShellKit.Shell.currentbut the generator'sgates(...)policy only covered FileManager path-arg methods, URLSession URL-arg methods, and theString/Datafile-IO entry points. Every other Foundation IO door —FileHandle(forReadingAtPath:),Bundle(path:),InputStream(url:),FileWrapper(url:options:),Process()— still reached the host's real disk / process table even under a rooted Sandbox. This PR closes the gaps listed in #3's "Known gaps" table.Summary
FileHandle,Bundle,InputStream,OutputStream,FileWrappereach get path/URL gating ingates(...)(so a script can't open a file outside the sandbox via a class init).Processis denied entirely whenever a sandbox is bound, via a newProcessSandboxDeniederror fromdenyProcessIfSandboxed()—Foundation.Processspawns a real OS subprocess that escapes every host gate, and ShellKit's closure-basedProcessTabledoesn't take a path-to-exec, so there's nothing to redirect to.pathStringLabelsRead/WriteandurlLabelsRead/Writesets describe whichString/URLarg labels carry filesystem paths;scanByLabelwalks every parameter position, not just index 0. This closes existing holes around two-path FileManager methods that PR Adopt ShellKit: route IO / FS / network / identity / exit through Shell.current #2 missed:setUbiquitous(_:itemAt:destinationURL:)(URL args at indices 1+2),createSymbolicLink(at:withDestinationURL:)(both URLs are writes),contentsEqual(atPath:andPath:)(the second path was ungated). The receiver rule supplies a default intent; the label scan upgrades it (e.g.to:→.fsWriteeven when the receiver default is read).NetworkConfig.checkAllowed. Previously an unrecognised method silently fell back to.GET, evaluating the request against GET permissions while sending the actual verb — a policy-bypass.URLRequest.httpBodyStream: InputStream?,URLResource.bundle: Bundle.Tests
ShellKitIntegrationTestsadds deny + allow paths for each new receiver:FileHandle(forReadingAtPath:),(forWritingAtPath:)Bundle(path:),(url:)InputStream(fileAtPath:),(url:)OutputStream(toFileAtPath:),(url:append:)FileWrapper(url:options:)Process()deny when sandbox is activeNotes
Bridges regenerated via
Tools/regen-foundation-bridge.sh. The bulky reorder noise inFoundationBridges.swiftandStdlibBridges.swiftis generator-output non-determinism (dict iteration order) — the only truly-new lines inFoundationBridges.swiftare the gate-injected bodies for the new receivers (~54 lines). Sorting the generator's dict output deterministically is a follow-up.Test plan
swift test --no-parallelcovers each new gated receiver (added inShellKitIntegrationTests)🤖 Generated with Claude Code