Sandboxing on macOS
Background
This is an overview of macOS's built-in support for application sandboxing. It covers how sandboxing behaves from an application's perspective, how sandbox policies are expressed, and how they're enforced by the macOS kernel. The goal is to help developers for non-Mac platforms understand what sandboxing entails on the Mac, and to provide macOS developers with a deeper understanding of how sandboxing works under the hood.
While I discuss sandboxing here in the context of macOS, much of the implementation and resulting behavior are shared with Apple's other platforms (iOS, iPadOS, tvOS, etc). The most significant differences are that on those other platforms, sandboxing is mandatory for third-party applications, and there is no support for using custom sandbox policies in third-party applications.
What is sandboxing?
Sandboxing a process is a means of placing hard limits on the operations it can perform. An application may be sandboxed for one of two reasons:
-
Security hardening
In this context, the goal is to limit the impact of an attacker gaining code execution within a sandboxed process. It places additional barriers between the initial code execution and the ability for the attacker to execute other applications or access resources such as user data on disk. This is the motiviation for sandboxing processes like web browser rendering engines and other applications that process complex data from from the internet.
-
User privacy
For many third-party applications, sandboxing is a demonstration that they take user privacy seriously rather than a security mitigation. This is particularly true of applications that use the App Sandbox policy as it places strict limits on which parts of the filesystem an application can access without the user explicitly granting them access via the system Open dialog.
Real-world sandbox policies block most operations by default and contain an allow-list of permitted operations. In a perfect world, the sandbox for a process is designed such that only the resources or operations needed during normal execution of the process are available to it. In practice, the large amount of code used in a typical process, both within the application itself and provided by operating system libraries, make it difficult to tailor such a tight sandbox. As a result most sandboxes evolve over time as the resources accessed by an application change or the resource usage by system libraries becomes better understood.
Sandboxing on macOS
Sandboxing on macOS is implemented via the Sandbox kernel extension and controlled via the
sandbox(7)
family of userspace APIs.
Sandboxing from an application's perspective
Types of application sandbox
There are two main ways that an application can run in a sandbox:
-
It can explicitly apply a sandbox to itself using the
sandbox(7)
family of APIs, most of which are undocumented. These APIs provide full control over the policy that is applied. -
It can opt into the App Sandbox via an entitlement. The App Sandbox is a predefined sandbox policy provided by macOS that uses process attributes, such as the presence of specific entitlements, to determine what resources should be accessible within the sandbox. Use of the App Sandbox is required for third-party applications distributed via the Mac App Store.
The opaque and inflexible nature of the App Sandbox means that many large, third-party applications that are distributed outside of the Mac App Store choose to use custom sandox policies rather than the App Sandbox.
How does an application become sandboxed?
An application using the App Sandbox entitlement will be sandboxed automatically
by initializers in libSystem
that run very early during an application's launch.
An application not using the App Sandbox entitlement must explicitly apply a
sandbox policy to itself using an API such as sandbox_init_with_parameters
.
It is not possible to sandbox an existing process from the outside. The process must apply the sandbox to itself.
Once a process has a sandbox applied to it, it is not possible for it to disable or remove the sandbox, nor is it possible for it to apply additional sandbox policies. Sandboxes can be inherited when a subproces is spawned by a sandboxed process.
What happens if the sandbox policy is violated?
By default, violating a sandbox policy results in the system call, or other operation that triggered
the violation, failing with EPERM
(operation not permitted). It is up to the calling code to handle
this error gracefully.
The sandbox kernel extension also logs a message to the system console, and sandboxd
(a userspace helper) may generate a violation report that includes a backtrace of
the thread that triggered the violation. This can be used to track down what code was
responsible for the violation.
Sandbox policies can control this behavior via action modifiers. See Sandbox policy evaluation for more detail.
What does a sandbox policy look like?
Sandbox policies are written in a dialect of Scheme known as SBPL. The policies
are interpreted via an interpreter within libsandbox
, based on
TinyScheme, which generates a
compiled representation of the policy that it passes to the Sandbox kernel
extension.
;; All operations not explicitly allowed will be denied.
;; Allow reading from /tmp/foo and the directory /tmp/bar
;; and any files below it.
;; Allow sysctl kern.hostname, but do it noisily.
;; Allow creating new files below /tmp/no-symlinks as long
;; as they aren't symlinks.
This simple policy shows the basics of how a sandbox policy is expressed. The
policy for individual operations is specified via an allow
or deny
function
call that takes the name of the operation and any predicates that must be
matched for the specified action (allow
or deny
) to be applied. A default
action is provided via the default
function for cases not matched via explicit
allow
or deny
actions.
Being a Scheme dialect, the usual programming language constructs are available: conditionals, loops, lambdas, and even macros!
Additionally, SBPL supports passing string values as parameters to the policy.
These parameters are available via the param
function during policy
evaluation. This combination of features makes it possible to express a policy
that the application can configure:
Sandbox extensions
The evaluation-time configurability that parameters and conditional logic provides is applicable to policy decisions that affect the lifetime of the process. However, they are not sufficient to handle cases where the possible resources that may be accessed are not known in advance. This requires the ability to dynamically extend the sandbox after a policy has been applied.
For example, a service's sandbox policy may be configured to allow it to access data that it owns that lives in a fixed location. Clients of the service may need it to perform work on data that they own that lives outside of that location. Rather than having a broad sandbox policy that makes the client's data available, the sandbox policy can be limited to only the service's data, and be extended dynamically to access the client's data via a sandbox extension.
Using a sandbox extension requires cooperation from both the sandbox policy, the sandboxed process, and and the process interacting with the sandboxed process.
The process interacting with the sandboxed process issues an extension of a given class (represented as a reverse-DNS style dotted string) to a given resource that it has access to (typically a path or Mach bootstrap name). This results in a sandbox token, represented as an opaque string.
The client sends this token to the sandboxed process which consumes the extension token. The effect that the extension has on the sandboxed process is controlled by logic within its sandbox policy. Once the work that requires the extended sandbox is finished, the sandboxed process can release the extension. This revokes access to the resources that the extension covered.
As an example of how the sandbox policy controls the effect an extension has on it, consider the following:
;; Allow reading any file for which we have consumed an
;; extension of class com.apple.app-sandbox.read
;; In contrast, the com.example.sandbox.read-downloads
;; extension will only permit reading a file if the extension
;; was issued for a path under ~/Downloads.
In the second example, the policy only permits the
com.example.sandbox.read-downloads
sandbox extension class to be used to
extend access to a specific subdirectory.
Sandbox APIs
All of the sandbox APIs described here are private APIs. Apple considers sandboxing at this level to be deprecated in favor of the App Sandbox. In practice, the App Sandbox is not a usable replacement for many large applications. Apple continues to make use of these lower-level APIs for sandboxing its first-party applications and helper tools.
sandbox_init_with_parameters
takes SBPL source as a string and an array of
parameters that the policy can access. It evaluates the SBPL and compiles the
resulting state to bytecode (described below). It then asks the Sandbox kernel
extension to apply the sandbox to the current process.
sandbox_compile
and sandbox_apply
split the work of compiling the policy to
bytecode and applying it to the process into two separate calls. This can avoid
the overhead of repeatedly evaluating the same SBPL source if you ever launch
more than one process with the same policy. Instead you can cache the compiled
bytecode and the process can then use the bytecode when applying its sandbox.
sandbox_extension_issue_file
, sandbox_extension_consume
,
sandbox_extension_release
are used for issuing, consuming, and releasing
extensions respectively.
MAC in the macOS kernel
The macOS kernel (XNU) provides a Mandatory Access Control Framework (MACF) that exposes around 300 policy hooks that can be used to approve or deny specific operations at a fine-grained level. Most of the policy hooks correspond to specific system calls or operations on the kernel's file system abstraction (VFS). As the name implies, these policy hooks are mandatory and are applied to all clients that use the system calls or perform file system operations.
The Sandbox kernel extension is a client of the MACF and implements many of the policy hooks exposed by the kernel. When a system call is made that the Sandbox kernel extension provides a policy hook for, XNU calls the corresponding policy hook early in the handling of the system call. The policy hook is provided with context about the operation being performed (i.e., arguments for the system call being made). This gives the Sandbox kernel extension an opportunity to deny the operation if its policies state that it should not be permitted.
Sandbox kernel extension
Sandbox policies can be applied to a process at two main levels[1]:
- The platform sandbox policy is applied to all processes.
- User-space applications can opt into being sandboxed.
When a user-space application opts into being sandboxed, its sandbox policy is
passed to the Sandbox kernel extension via the __mac_syscall
system call
(often via the __sandbox_ms
wrapper function). The kernel routes this system
call to the appropriate MACF client based on its arguments. The Sandbox kernel
extension performs some basic validation and then associates the policy with the
kernel proc
structure for the process using a MAC label.
When the kernel calls a MACF policy hook in the Sandbox kernel extension, the policy hook is mapped to a sandbox operation type that describes the operation being attempted. This provides a layer of indirection from the exact MACF hooks exposed in the kernel. You can see the complete list of sandbox operations here.
The operation is then evaluated against the platform sandbox policy. If the system sandbox policy denies the operation, the denial is propagated back through the MACF hook to the kernel and an error is returned via the system call.
If the platform sandbox policy permits the operation, it will next be evaluated against the process sandbox policy. The Sandbox kernel extension retrieves the sandbox policy associated with the process performing the hooked operation. If a sandbox policy is found, it is evaluated to determine whether the operation should be permitted. If the process is not sandboxed, the operation is permitted.
Sandbox policy evaluation
The sandbox policy consists of a list of bytecode instructions, and a mapping from sandbox operation to the initial bytecode instruction to be evaluated for that operation. Bytecode instructions take two forms:
-
Filter instructions
These consist of a predicate, an if match instruction index, and an if not match instruction index. If the predicate evaluates to true, execution continues from the if match instruction index. Otherwise it jumps to the if not match instruction index.
-
Action instructions
Actions may be either
allow
ordeny
, and they may have one or more modifiers associated with them. When evaluation reaches an action, the modifiers are applied and evaluation of the sandbox policy terminates with the action as the result.
In the policies I have analyzed, all jumps are forward jumps. As a result the bytecode forms a disconnected directed graph from entry points, through zero or more filters, to actions.
Filters
There are around 90 different filter predicates supported as of macOS 15. You can see the complete list of filter predicates here.
Most predicates match information about the operation being performed. For
instance, file system operations can be filtered via the path
and vnode-type
filters. sysctl
system calls can be filtered using the name of the operation via the
sysctl-name
predicate. Mach bootstrap lookup operations can be matched via the
global-name
or local-name
filters.
The remainder of the predicates deal with attributes of the process performing
the operation, such as signing-identifier
or entitlement-is-present
, or the
state of the operating system system (csr
for information about configurable
security restrictions such System Integrity Protection, or system-attribute
for other attributes).
These last two groups of predicates are mostly of use to policies that apply to more than one application, such as the platform sandbox policy or the App Sandbox, rather than application-specific policies.
Action modifiers
Modifiers can be applied on both allow
or deny
actions in order to modify
the default behavior. The
permitted set of modifiers differs for allow
and deny
actions as some
modifiers only make sense in one context. You can see the complete list of
action modifiers
here.
Examples of modifiers supported on deny actions include:
- overriding the errno value that is returned from the denied operation:
(with EBADEXEC)
- suppressing reporting of sandbox violations:
(with no-report)
- sending a signal to the user-space thread that performed the system call:
(with send-signal SIGUSR1)
- triggering Apple-internal telemetry related to the violation:
(with telemetry)
Modifiers supported on allow actions include:
- generating a violation report:
(with report)
- logging a message to the system console:
(with message "…")
- triggering Apple-internal telemetry related to the operation:
(with telemetry)
-
I have come across references to a few other types of policies (autobox, bastion, and delegated policies), but I have not yet investigated what role they play. ↩