How is the sandbox actually enforced, what does a denied operation look like, and what does the simulator not enforce?
Predict first
What does the same program report with and without a sandbox profile (macOS, sandbox-exec)?
// === sb.swift ===
import Foundation
func attempt(_ path: String) -> String {
let fd = open(path, O_WRONLY | O_CREAT, 0o644)
if fd < 0 { return "denied, errno \(errno): \(String(cString: strerror(errno)))" }
close(fd); unlink(path); return "allowed"
}
print("write in tmp:", attempt(NSTemporaryDirectory() + "sb-check.txt"))
print("write in home:", attempt(NSHomeDirectory() + "/sb-check.txt"))
let p = Process(); p.executableURL = URL(fileURLWithPath: "/usr/bin/true")
do { try p.run(); p.waitUntilExit(); print("spawn /usr/bin/true: allowed") } catch { print("spawn /usr/bin/true: denied") }
// === p.sb ===
(version 1)
(allow default)
(deny file-write* (subpath "HOMEDIR"))
(deny process-exec* (literal "/usr/bin/true"))
// === run.sh ===
swiftc -swift-version 5 sb.swift -o sb 2>&1 | grep error
sed "s|HOMEDIR|$HOME|" p.sb > p2.sb
echo "-- without a profile"; ./sb
echo "-- under a profile"; sandbox-exec -f p2.sb ./sb
- Without a profile everything is allowed; with the profile the home write is denied with errno 1 and the spawn is refused, while the temp write still works
- Both runs allow everything because sandbox-exec only logs
- Both runs deny the home write
Mental model. The sandbox is a locked door the building enforces, not a sign asking you to stay out. Your code can try the handle as often as it likes; the door answers “no” in the same way every time.
The iOS sandbox is the same Seatbelt mechanism that exists on macOS: a profile is attached to the process at launch and the kernel checks it on each operation (opening a file, forking, executing, connecting to a Mach service). It is not an API your code calls, so you cannot bypass it by avoiding FileManager. A denied operation fails in the ordinary way: the system call returns an error with errno 1, EPERM “Operation not permitted”, which surfaces in Cocoa as NSCocoaErrorDomain 513 (no permission) or a POSIX error. The check runs the same program twice on macOS, once free and once under a profile that denies writes under the home directory and executing /usr/bin/true: the temp-directory write still works, the home write fails with errno 1, and the spawn is refused. That is the macOS tool sandbox-exec standing in for the profile an iOS app gets; it shows the mechanism, not the actual iOS profile, which is not public.
Two practical points. First, the iOS Simulator does not sandbox apps: simulated processes are ordinary macOS processes, with a data container directory by convention only, so code that reads or writes outside the container can work in the simulator and fail on a device. Test file access, extension data sharing and entitlement-dependent features on hardware. Second, a failed operation that is not an EPERM may still be the sandbox: system services check the caller’s entitlements or Mach-service access and answer with their own errors (a keychain status, a connection invalidated, an empty result). When a call works in the simulator and fails on a device with an unhelpful error, the device console logs lines from sandboxd or the kernel saying which operation was denied; search for “deny” with the process name. On a device there is no way for an app to widen its own sandbox: only entitlements, granted through the signed profile, change it (Q209).
The kernel checks the profile on each operation and answers EPERM; unrelated operations are unaffected.
Common traps
- Trusting the simulator for file-access behaviour. It is not sandboxed, so out-of-container accesses wrongly succeed.
- Handling only the error text. Inspect the error domain and code (
EPERM,NSCocoaErrorDomain513) so a denied operation is not retried forever.
They might follow up with
- Can an app detect that it is sandboxed? It always is on iOS; on macOS the
APP_SANDBOX_CONTAINER_IDenvironment variable and the entitlement indicate it. - Where do sandbox denials appear? In the device console and in the sysdiagnose, as
Sandbox: process(pid) deny(1) operationlines.
Related questions
- 6.1.1 What is the iOS process model, and what can an app process not do?
- 6.1.2 What is the app container, and why must you never persist an absolute path?
- 6.1.3 What are entitlements, and what happens when they do not match the provisioning profile?
← Which memory number does the system judge your app by, and why do…