Cordon::SandboxExec
Inherits Cordon::Runner < Reference < Object
macOS sandbox runner using sandbox-exec and SBPL profiles.
sandbox-exec wraps a process in Apple's Seatbelt framework (a MACF kernel module). It evaluates a declarative SBPL (Sandbox Profile Language) policy against every syscall the process makes; violations return EPERM.
SBPL is a Scheme-like DSL. This runner generates a deny-default profile and adds explicit allow rules derived from the policy.
Deprecation note: sandbox-exec has been marked deprecated in macOS headers since 10.8 but remains functional through current releases. No public replacement exists for the ad-hoc CLI use case (App Sandbox requires code signing and an app bundle). Used in production by Chromium and Firefox.
The BASELINE constant contains the minimum permissions any process needs to start under deny-default. Omitting any of these typically causes an immediate crash or silent hang (dyld, Mach IPC, and sysctl are all gated).
process-exec / process-exec-interpreter are deliberately NOT part of
BASELINE. (allow process-exec) with no path filter permits exec'ing ANY
binary on the filesystem, regardless of file-read* restrictions elsewhere
in the profile — Seatbelt does not cross-reference the two independent
permissions, and (deny default) does not implicitly couple "readable"
with "executable". A read-only or read-write policy that only grants
access to a workspace directory would still allow exec'ing e.g.
/usr/bin/ruby, /opt/homebrew/bin/, or any other binary on the system —
entirely bypassing the intended containment. process-exec-interpreter
(governs the interpreter named on a script's #! line) is the same class
of bypass reached via a script instead of a direct exec, so it gets the
same treatment. Both are scoped per-profile in #generate_profile instead,
to exactly the paths already granted file-read (read-only/read-write/
tmpfs) — and nothing else.
In particular, the target command's own binary is NOT granted an implicit exception. The policy defines the perimeter; a command that lives outside it must be rejected, not admitted on the grounds that it was the command asked for. Naming a binary is not authority to run it — under the agent threat model the command string is precisely what the untrusted party controls, so honouring it would let the sandboxed side choose its own escape. Running a command therefore requires that the policy already cover it (via read_only/read_write/tmpfs, Preset::System, or a toolchain preset). Even system binaries like /bin/sh are NOT exec-able by default. See DEVELOPMENT.md → "Why process-exec is not in BASELINE".
Constants
Instance methods
Replaces the current process with command, inside the sandbox described by policy. Used for self-relaunch (Cordon.relaunch) — the caller does not resume; either the sandboxed command takes over the process image, or this raises.
Returns the SBPL profile string for policy. Useful for inspection, logging, or writing to disk without executing.
Depends only on policy — the profile is the same whatever command is
eventually run under it, since no command receives an implicit grant
(see BASELINE's comment). This makes the profile fully inspectable
ahead of time: what cordon inspect prints is what will be enforced.
All paths are resolved to their real, symlink-free form before being written to the profile (see #resolve_path) — SBPL matches against the path the kernel actually resolves to, not the string the caller wrote. "./" or "~/.rubies" pointing through a symlink would silently match nothing otherwise.
Short platform-facing name for this runner (e.g. "bwrap", "sandbox-exec"), used in ConfirmReport output.
Runs command inside the sandbox described by policy.
If shell is true, command must be a single-element array holding
a full shell script string, executed via /bin/sh -c. No
positional-arg forwarding is supported — build the complete script
string yourself before calling. /bin/sh must be exec-granted by
policy (e.g. via Preset::System), same as any other target;
shell: true is not an implicit exec exception.
If shell is false (the default), command is the literal argv — command[0] is exec'd directly with command[1..] as its arguments, no shell involved.