Sandbox profiles: SBPL and kernel evaluation
Inside the macOS sandbox — a Scheme-derived policy language, a compiler in libsandbox, and a kernel evaluator that runs on every controlled syscall.
Every App Store app runs sandboxed. Most system daemons run sandboxed. Safari's web content processes run aggressively sandboxed. The sandbox is mentioned everywhere on this site as "the kernel checks", but what does that actually mean? What's the language, where does it compile, and what is the kernel doing on every syscall?
This article is the deep dive.
What a sandbox profile looks like
Profiles are written in SBPL — Sandbox Profile Language — a Scheme-derived DSL. A simplified, real-shaped example:
(version 1)
(deny default)
; Always allow basic process operations
(allow process-fork)
(allow process-exec)
(allow signal (target self))
; File access
(allow file-read* (subpath "/usr/lib"))
(allow file-read* (subpath "/usr/share"))
(allow file-read* (subpath "/System/Library"))
(allow file-read-data (subpath "/Library/Caches"))
(allow file-write* (subpath (param "_HOME") "/Library/Caches"))
; IPC
(allow mach-lookup (global-name "com.apple.distributed_notifications"))
(allow mach-lookup (global-name "com.apple.cfprefsd.agent"))
; Network: client only, no listening
(allow network-outbound)
Each rule is (allow|deny) <operation> <filters>.... Operations cover every controlled syscall class: file-*, mach-*, signal, process-*, iokit-open, network-*, system-info, plus dozens more.
Filters can match by path subpath, by literal path, by regular expression, by Mach port name, by file extension, by xattr, and so on. Filters compose with (require-all …) / (require-any …).
The DSL also supports (import "/System/Library/Sandbox/Profiles/system.sb") so profiles can build on shared bases — the system ships a handful of canonical base profiles every per-app profile inherits from.
Where profiles live
A few places to look:
/System/Library/Sandbox/Profiles/— base profiles for Apple's own daemons. Every running system daemon's profile is in here./usr/share/sandbox/— older base profiles, less used on modern systems.- Embedded in apps — App Store apps embed their profile in the
Info.plist(keycom.apple.security.app-sandbox = true+ a list of granted entitlements that turn into profile rules). sandbox-exec -p '<profile>' command— apply an inline profile to a child invocation. Useful for testing.
You can read the system base profiles directly:
cat /System/Library/Sandbox/Profiles/com.apple.Safari.sb
(Some of these are signed and may require SIP-disabled inspection, but most are readable.)
The compiler — libsandbox
When a process applies a profile, the textual SBPL doesn't go straight to the kernel. libsandbox in userspace compiles the profile into a compact bytecode the kernel can evaluate efficiently:
- The Scheme-like text is parsed.
- Filters are normalized (subpath matching is converted to path-prefix trees, regex to compiled NFAs).
- The rules are emitted as bytecode operations the kernel evaluator understands.
- The bytecode is handed to the kernel via
sandbox_apply().
The compiler itself is closed source (part of libsandbox.dylib), but you can extract a compiled profile with sbutil and disassemble it for inspection.
The kernel side — Sandbox.kext
Once the bytecode is loaded into the kernel, Sandbox.kext owns it for the rest of the process's lifetime. It registers as a MAC framework policy:
apple-oss-distributions/xnusecurity/mac_framework.hThe MAC (Mandatory Access Control) framework — XNU's hook system for security policies. Sandbox.kext plugs in here.View on GitHub(line —) apple-oss-distributions/xnusecurity/mac_policy.hThe full list of MAC hooks — every controlled syscall has one.View on GitHub(line —)
The MAC framework defines hundreds of hooks. A few examples:
mpo_vnode_check_open— fires when a process opens a file.mpo_proc_check_signal— fires when one process signals another.mpo_socket_check_bind— fires when a process binds a socket.mpo_iokit_check_open— fires when a process opens an IOKit user client.
Every hook is called on every relevant syscall, with the current process and the operation's parameters. The sandbox's policy callback runs the loaded bytecode against the parameters and returns allow / deny.
What evaluation actually looks like
For a file-open operation on /Library/Foo/bar.txt:
- Process calls
open("/Library/Foo/bar.txt", O_RDONLY). - BSD's
openhandler does path resolution, gets a vnode. - BSD calls
mac_vnode_check_open(cred, vnode, "/Library/Foo/bar.txt", O_RDONLY). - MAC framework iterates registered policies; Sandbox.kext's callback runs.
- The callback walks the sandboxed process's compiled rules looking for matches. Path-prefix tree lookup, predicate evaluation, allow/deny decision.
- If denied,
openreturnsEPERM— userspace sees a normal POSIX error.
Cost per check: nanoseconds for cached / simple matches, microseconds for complex ones. The kernel caches recent decisions per-process to amortize cost across repeated access patterns.
The Helper-process trick
A common pattern: an app process wants to access a file the sandbox blocks. The app extends a privileged operation to a helper process with a more permissive profile, communicating via XPC.
The kernel verifies the helper is signed by the same team as the app (via code signing + library validation). The helper has its own sandbox profile that allows the operation. The app sends the request over XPC; the helper does the work and returns the result.
This is how Safari can render arbitrary web content (in a render process with a brutal sandbox) but still access bookmarks (in the main process with a normal sandbox) — each subsystem runs in the tightest profile it can.
Entitlement-driven profile rules
App Store profiles aren't hand-written for each app. They're generated from the entitlements the app declares:
com.apple.security.network.client→ adds(allow network-outbound).com.apple.security.files.user-selected.read-write→ adds(allow file-read-write* (extension "com.apple.app-sandbox.read-write")).com.apple.security.device.camera→ adds rules to open the camera IOKit user client.
Each entitlement maps to a specific profile fragment. The app's full effective profile is the App Store base + per-entitlement fragments. This is why entitlements are signed in — adding an entitlement post-signing would skip the kernel's check that the signature actually grants the rule.
Common surprises
- There is no "remove sandbox" option for an App Store app, even with admin. The kernel enforces the profile from
execonward; no userspace can lift it. - The default deny is
denyish, notdeny. Most base profiles allow a lot by default and selectively deny. - Profiles are read once at exec, then immutable for the process's lifetime. No reload. The app would have to be re-launched to get a new profile.
sandbox-execfor testing requires SIP-relaxed on modern macOS — Apple wants you to express constraints via entitlements, not ad-hoc profiles.
What to read next
apple-oss-distributions/xnusecurity/mac_vfs.cMAC file-system hooks — most file-related sandbox checks bottom out here.View on GitHub(line —) apple-oss-distributions/xnusecurity/mac_mach.cMAC Mach-port hooks — controls mach-lookup, mach-register, exception ports.View on GitHub(line —)
And the security architecture article for where the sandbox sits in the layered policy stack.