21 minute read

Last time, we discussed how Apple uses macOS filesystems as a protection mechanism. We also introduced CSR and the csrutil commands used to inspect how the kernel enforces these policies.

This time, we will go a little deeper into both SIP’s internal mechanisms and APFS-related defences.

SIP

Let’s demystify the System Integrity Protection mechanism, or SIP for short.

SIP is quite often called Rootless because of its perceived effect, not because of its actual nature and function. It is not a “lobotomised root” — there are already large crowds of users with root privileges for whom lobotomy would be a major upgrade. SIP is a set of restrictions enforced by the XNU kernel and systematically applied to all processes on the machine. root, launchd, and even some kernel operations are affected by SIP.

SIP is not a mechanism that prevents root from making a mess. That is merely a consequence. SIP acknowledges that the traditional Unix security model is no longer sufficient. In a traditional Unix model, authority is managed hierarchically: root is at the top of the chain, and everything else is subject to its authority. The downside? If you pwn the root account, you own the machine.

SIP breaks this structure by introducing a parallel authority: there are operations that root cannot perform, not because of a lack of privileges, but because the kernel has a different policy, kept separate from POSIX policies, that forbids them.

Technically, SIP “lives” in two different places. People quite often miss this, so it is worth making it clear once and for all.

  1. The csr-active-config flag, stored in NVRAM, contains the configuration bitfield. This specifies what is currently permitted by SIP.
  2. Several checks exist in different kernel components, such as MACF policies, the VFS layer, task ports, the kext loader, dtrace, and so on. These entities read that bitfield via csr_check() and csr_get_active_config() and decide whether the current policy allows the requested operation.

From this, it should be clear that SIP itself is not a module, intended as a “single entity”. It is more of a policy, spread throughout the whole system, that the kernel applies in different parts of its own code. In fact, older SIP bypasses worked not by “breaking SIP”, but rather by finding a single point where SIP controls did not work properly, or where an Apple entitlement allowed them to be bypassed.

It is a paradigm shift, a different model; definitely not just another set of features. The kernel asserts that your POSIX identity is not enough to decide what your process is allowed to do. It must also understand what you are, not only your asserted identity. This what you are is determined by Apple-signed entitlements, not by a simple UID.

SIP is not only filesystem-oriented

SIP does not protect files. It protects categories.

Of course, the filesystem can be protected by SIP. Later on, we will see files protected with the restricted flag. This is the most famous part.

Another category is processes. If you have read Advanced Apple Debugging & Reverse Engineering (Fourth Edition): Exploring Apple Code Through LLDB, Python & DTrace, you have already been exposed to the concept: SIP prevents attaching a debugger to protected processes — generally Apple-signed processes with the hardened runtime enabled. Not even root can do it. That is why you cannot lldb Finder or WindowServer.

Less famous are task ports. In Mach, a process task port is how you actually read and write that process’s memory. SIP determines who can obtain task ports. Without this restriction, the whole code-injection prevention mechanism of macOS would be radically different.

Kernel extensions, or kexts, are also affected by SIP, which controls which ones can be loaded. The kext signing and kext loading from approved locations aspects are de facto SIP.

Then there is Non-Volatile RAM, or NVRAM. Some variables stored in NVRAM are SIP-protected. You cannot change boot-args with SIP enabled. Fortunately, because boot-args has historically been a vector for disabling kernel protections.

Also, DTrace is limited by SIP. SIP defines what DTrace can and cannot inspect. To my pain — and to the pain of other analysts — dtrace used to be an… unfair superpower.

Finally, some Apple-signed .app bundles are SIP-protected.

Roughly speaking, most of these categories map to bits in the csr.h XNU header file, which can be found here. These bits are exposed through the csr-active-config value. For convenience, the key values are:

bit bit # action
CSR_ALLOW_UNTRUSTED_KEXTS 0 Allows loading unsigned kexts
CSR_ALLOW_UNRESTRICTED_FS 1 Bypasses filesystem protections, including restricted paths
CSR_ALLOW_TASK_FOR_PID 2 Allows access to protected task ports
CSR_ALLOW_KERNEL_DEBUGGER 3 Allows kernel debugging
CSR_ALLOW_APPLE_INTERNAL 4 Enables Apple-internal behaviour. Usually 0 on retail Macs
CSR_ALLOW_UNRESTRICTED_DTRACE 5 Allows unrestricted dtrace
CSR_ALLOW_UNRESTRICTED_NVRAM 6 Allows unrestricted writes to NVRAM
CSR_ALLOW_DEVICE_CONFIGURATION 7 Allows device configuration changes. Rarely relevant for normal analysis
CSR_ALLOW_ANY_RECOVERY_OS 8 Allows booting from an arbitrary recoveryOS
CSR_ALLOW_UNAPPROVED_KEXTS 9 Allows loading approved, but non-Apple, kexts
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE 10 Allows executable policy overrides
CSR_ALLOW_UNAUTHENTICATED_ROOT 11 Allows booting from non-sealed snapshots. This is the SSV-related bit from Big Sur onward
CSR_ALLOW_RESEARCH_GUESTS 12 Allows research guests

So, SIP does not “live in a file”; rather, its access points are csr_check(mask) and csr_get_active_config(). But the call sites are everywhere: in the VFS layer, when something tries to write to SF_RESTRICTED paths; in the Mandatory Access Control Framework (MACF), through AMFI and similar policies; in the kext loader (OSKext); and so on.

This is why SIP can be fragile against bypasses. There is no single choke point, but a multitude of them. Every call site is a potential bug.

Migraine (New macOS vulnerability, Migraine, could bypass System Integrity Protection) exploited systemmigrationd; Shrootless (Microsoft finds new macOS vulnerability, Shrootless, that could bypass System Integrity Protection) exploited system_installd.

Both had the com.apple.rootless.install.heritable entitlement, which means that csr_check() would grant permission to the spawned processes. Neither really broke SIP. It was enough to find an Apple process entitled to bypass SIP and use it to do the dirty work.

You want to see this for yourself? Feel free to adapt the C-code below (again, I can program, therefore I don’t do Python)

Note: I forgot to add CSR_ALLOW_RESEARCH_GUESTS, but adapting the code is a one-line exercise. Sorry. It was not relevant for our discussion, anyway.

#include <stdio.h>
#include <stdint.h>

extern int csr_get_active_config(uint32_t *config);

/* CSR bit definitions, from osfmk/sys/csr.h in XNU */
#define CSR_ALLOW_UNTRUSTED_KEXTS               (1 << 0)
#define CSR_ALLOW_UNRESTRICTED_FS               (1 << 1)
#define CSR_ALLOW_TASK_FOR_PID                  (1 << 2)
#define CSR_ALLOW_KERNEL_DEBUGGER               (1 << 3)
#define CSR_ALLOW_APPLE_INTERNAL                (1 << 4)
#define CSR_ALLOW_UNRESTRICTED_DTRACE           (1 << 5)
#define CSR_ALLOW_UNRESTRICTED_NVRAM            (1 << 6)
#define CSR_ALLOW_DEVICE_CONFIGURATION          (1 << 7)
#define CSR_ALLOW_ANY_RECOVERY_OS               (1 << 8)
#define CSR_ALLOW_UNAPPROVED_KEXTS              (1 << 9)
#define CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE    (1 << 10)
#define CSR_ALLOW_UNAUTHENTICATED_ROOT          (1 << 11)

struct csr_bit {
    uint32_t mask;
    const char *name;
};

static const struct csr_bit bits[] = {
    { CSR_ALLOW_UNTRUSTED_KEXTS,            "CSR_ALLOW_UNTRUSTED_KEXTS" },
    { CSR_ALLOW_UNRESTRICTED_FS,            "CSR_ALLOW_UNRESTRICTED_FS" },
    { CSR_ALLOW_TASK_FOR_PID,               "CSR_ALLOW_TASK_FOR_PID" },
    { CSR_ALLOW_KERNEL_DEBUGGER,            "CSR_ALLOW_KERNEL_DEBUGGER" },
    { CSR_ALLOW_APPLE_INTERNAL,             "CSR_ALLOW_APPLE_INTERNAL" },
    { CSR_ALLOW_UNRESTRICTED_DTRACE,        "CSR_ALLOW_UNRESTRICTED_DTRACE" },
    { CSR_ALLOW_UNRESTRICTED_NVRAM,         "CSR_ALLOW_UNRESTRICTED_NVRAM" },
    { CSR_ALLOW_DEVICE_CONFIGURATION,       "CSR_ALLOW_DEVICE_CONFIGURATION" },
    { CSR_ALLOW_ANY_RECOVERY_OS,            "CSR_ALLOW_ANY_RECOVERY_OS" },
    { CSR_ALLOW_UNAPPROVED_KEXTS,           "CSR_ALLOW_UNAPPROVED_KEXTS" },
    { CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE, "CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE" },
    { CSR_ALLOW_UNAUTHENTICATED_ROOT,       "CSR_ALLOW_UNAUTHENTICATED_ROOT" },
};

int main(void) {
    uint32_t c = 0;
    csr_get_active_config(&c);
    printf("CSR config: 0x%08x\n\n", c);

    for (size_t i = 0; i < sizeof(bits)/sizeof(bits[0]); i++) {
        printf("  [%s] %s\n",
               (c & bits[i].mask) ? "ALLOW" : "deny ",
               bits[i].name);
    }
    return 0;
}

You can compile this with clang -o csr csr.c (provided you called the file csr.c.). Running this small piece of code can be quite pedagogical - look:

gabriel@psychopompos Desktop % ./csr 
CSR config: 0x0000006f

  [ALLOW] CSR_ALLOW_UNTRUSTED_KEXTS
  [ALLOW] CSR_ALLOW_UNRESTRICTED_FS
  [ALLOW] CSR_ALLOW_TASK_FOR_PID
  [ALLOW] CSR_ALLOW_KERNEL_DEBUGGER
  [deny ] CSR_ALLOW_APPLE_INTERNAL
  [ALLOW] CSR_ALLOW_UNRESTRICTED_DTRACE
  [ALLOW] CSR_ALLOW_UNRESTRICTED_NVRAM
  [deny ] CSR_ALLOW_DEVICE_CONFIGURATION
  [deny ] CSR_ALLOW_ANY_RECOVERY_OS
  [deny ] CSR_ALLOW_UNAPPROVED_KEXTS
  [deny ] CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE
  [deny ] CSR_ALLOW_UNAUTHENTICATED_ROOT

we can cross check this with

gabriel@psychopompos Desktop % sudo bputil -d |grep -i SIP
Password:
SIP Status:                  Customized (sip0): 7f
Signed System Volume Status: Enabled    (sip1): absent
Kernel CTRR Status:          Disabled   (sip2): 1
Boot Args Filtering Status:  Disabled   (sip3): 1

Why the discrepancy (my code returned a 0x6f whilst bputil returned a 0x7f)? First, let’s decode the values and interpret them.

  • 0x6f is the hex for 0110 1111
  • 0x7f is the hex for 0111 1111
bit # 0x6f 0x7f key meaning 0x6f meaning 0x7f
0 1 1 CSR_ALLOW_UNTRUSTED_KEXTS ALLOW ALLOW
1 1 1 CSR_ALLOW_UNRESTRICTED_FS ALLOW ALLOW
2 1 1 CSR_ALLOW_TASK_FOR_PID ALLOW ALLOW
3 1 1 CSR_ALLOW_KERNEL_DEBUGGER ALLOW ALLOW
4 0 0 CSR_ALLOW_APPLE_INTERNAL DENY ALLOW
5 1 1 CSR_ALLOW_UNRESTRICTED_DTRACE ALLOW ALLOW
6 1 1 CSR_ALLOW_UNRESTRICTED_NVRAM ALLOW ALLOW
7 0 0 CSR_ALLOW_DEVICE_CONFIGURATION DENY DENY

CSR_ALLOW_APPLE_INTERNAL is the bit that marks Apple-internal systems: development boards, internal signing chains, non-customer builds. Things that live behind Apple’s walls, not on a customer’s desk.

So why does the LocalPolicy claim it is set, while the kernel says it is not?

Because that bit is not a permission switch. It is an identity claim. Setting CSR_ALLOW_APPLE_INTERNAL in a policy is like writing “I am the King of France” on your passport: the words are there, but the immigration officer will not bow. A retail Mac does not become an Apple-internal machine by flipping a bit, because being Apple-internal is a property of the hardware — fused identifiers, signing chains, ECIDs known to Apple — not of the configuration. The kernel knows it is running on retail silicon and silently refuses to honour the claim, regardless of what the signed policy says.

That refusal is the interesting part. We started this post observing that SIP introduces a parallel authority above root: the kernel policy, anchored to Apple keys, beyond the reach of the POSIX root. The 0x6f vs 0x7f discrepancy shows that even that policy does not have the last word. There is one more authority above it: the kernel’s runtime view of what the hardware actually is. A signed policy can authorise; the kernel can still veto.

The chain of authority on a modern Mac looks roughly like this:

  • root
    • SIP/Signed local policy
      • kernel runtime view
        • hardware identity

Each layer can constrain the one above it. Root cannot override SIP. SIP cannot override what the kernel decides to enforce. The kernel cannot override what the hardware says it is. bputil reads the second layer; our small piece of C code reads the third. They disagree, and the disagreement is structurally correct.

This is also why “disabling SIP” is a misleading shorthand. You are not turning off a feature. You are reconfiguring one layer of a stack — and the layers below it keep doing their job whether you like it or not. We saw the same pattern in the previous post: sip1 in the bputil output still reports the Signed System Volume as enabled, even on a machine with SIP broadly relaxed. The sealed snapshot does not care about your CSR bitfield. Different layer, different authority, different conversation.

So yes — it was worth writing that code. Not because the output is surprising, but because the discrepancy is. Two tools, two answers, and the gap between them is exactly where the real architecture lives.

The file rootless.conf

It is useful to introduce /System/Library/Sandbox/rootless.conf, because it gives us a firmer understanding of SIP and of the related mechanisms we discuss here and will discuss again in the future.

On a modern macOS, this file has a hundred lines more or less. The general syntax is:

<owner_token> <path>

owner_token identifies who’s entitled to change the path. These are symbolic tokens, corresponding to specific entitlements. Only Apple-signed processes carrying the matching entitlement can write to that path.

For instance, on my machine:

gabriel@psychopompos Desktop % cat  /System/Library/Sandbox/rootless.conf| head 
							/Applications/Safari.app
							/Library/Apple
CoreAnalytics				/Library/CoreAnalytics
NetFSPlugins				/Library/Filesystems/NetFSPlugins/Staged
NetFSPlugins				/Library/Filesystems/NetFSPlugins/Valid
							/Library/Frameworks/iTunesLibrary.framework
KernelExtensionManagement	/Library/GPUBundles
KernelExtensionManagement	/Library/KernelCollections
MessageTracer				/Library/MessageTracer
AudioSettings				/Library/Preferences/Audio/Data

Some lines appear without a leading owner_token: there is only a path. This means that no regular entitled owner is specified for that path. In practice, it cannot be modified except by processes carrying broad system entitlements such as com.apple.rootless.install.

Some paths are prefixed with *. This means that the path is a directory and that the protection applies to its contents, not just to the directory entry itself. The contents are not owned by any specific token. This pattern is typically used for cache or template directories that are populated and cleaned by the system.

gabriel@psychopompos Desktop % grep -E "^\*" /System/Library/Sandbox/rootless.conf | head    
*				/System/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment
*				/System/Library/Caches
*				/System/Library/Extensions
*				/System/Library/Speech
*				/System/Library/Templates/Data
*				/System/Library/Templates/Data/System/Library/Caches
*				/System/Library/Templates/Data/System/Library/Speech
*				/System/Library/Templates/Data/private/var/folders
*				/System/Library/Templates/Data/usr/libexec/cups
*				/System/Library/Templates/Data/usr/local

These two listings illustrate, for instance, that /Applications/Safari.app cannot be freely modified. This is consistent with the results shown in the previous posts, where rm -fr / did not delete Safari. On the other hand, /System/Library/Extensions is a directory that the system is allowed to populate.

If a path ends with /*, then the protection applies to the contents of the directory, not to the directory entry itself. In fact:

gabriel@psychopompos Desktop % cat  /System/Library/Sandbox/rootless.conf| grep -i extensions           
KernelExtensionManagement	/Library/StagedDriverExtensions
KernelExtensionManagement	/Library/StagedExtensions
*				/System/Library/Extensions
				/System/Library/Extensions/*
KernelExtensionManagement	/System/Library/Templates/Data/Library/StagedDriverExtensions
KernelExtensionManagement	/System/Library/Templates/Data/Library/StagedExtensions
KernelExtensionStaging		/private/var/db/KernelExtensionManagement/Staging

This shows that both /System/Library/Extensions itself and its contents are protected. The important detail is that both had to be listed explicitly. Together, the two lines express a recurring pattern: “the system populates, then nobody touches.”

Apple uses this pattern wherever it needs to install content first and make that content immutable afterwards.

Now, an interesting exercise:

gabriel@psychopompos Desktop % ls -lOd  /System/Library/Sandbox/rootless.conf
-rw-r--r--  1 root  wheel  restricted,compressed 5331 20 Mar 05:25 /System/Library/Sandbox/rootless.conf

Three flags here: -l for long format (the usual ls long listing), -O (capital O), which is the macOS-specific flag that exposes BSD file flags as an extra column — without it, restricted, sunlnk, schg, and friends are invisible — and -d, to inspect the directory entry itself rather than its contents. If you forget -d on a directory, you’ll see the flags of what’s inside, not of the thing you asked about.

However, what I wanted to show is that the file itself appears as restricted and hence SIP-protected. From this observation, a structural consideration regarding who protects the protectors descends. Perhaps we’ll do it in another post. But if we recall from the last post, /System/Library lives in a sealed volume; therefore, we have three levels of protection here:

  • the BSD flag restricted;
  • filesystem SSV policy, because the volume is sealed;
  • the Merkle tree that supplies the cryptographic seal for the file itself.

The foundation of the SIP policy is SIP-protected. I am a fan of the fans of recursion!

Since we have introduced them above, it’s now time to talk a little bit more about entitlements.

Who is entitled to what - the Entitlements model

Perhaps it’s redundant if you’re reading me, but let’s formalise this once for all. What is an entitlement. This is the typical term (ab)used in the macOS world - everybody seems knowing what they are until you speak to them. This is when they describe entitlements as their intuitive meaning. There’s obviously more to that.

Structurally speaking: an entitlement is a key-value pair of the form

property <-> value

stating that a given object has a specified characteristic, resulting in the ability of the object to perform a (set of) task. From a technical perspective, an entitlement is a property list embedded in the binary’s code signature. Property lists themselves come in two on-disk flavours, binary and XML, but when you ask codesign to dump them it has its own opinions about formatting. By default, you get a pretty-printed human-readable representation that is neither — something like:

gabriel@psychopompos Desktop % codesign -d --entitlements - /System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/Resources/system_installd

Executable=/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/Resources/system_installd
[Dict]
	[Key] com.apple.private.apfs.create-synthetic-symlink-folder
	[Value]
		[Bool] true
	[Key] com.apple.private.launchservices.cansetapplicationstrusted
	[Value]
		[Bool] true
	[Key] com.apple.private.package_script_service.allow
	[Value]
		[Bool] true
	[Key] com.apple.private.responsibility.set-arbitrary
	[Value]
		[Bool] true
	[Key] com.apple.private.responsibility.set-hosted-properties
	[Value]
		[Bool] true
	[Key] com.apple.private.security.storage-exempt.heritable
	[Value]
		[Bool] true
	[Key] com.apple.private.security.syspolicy.package-installation
	[Value]
		[Bool] true
	[Key] com.apple.private.security.syspolicy.package-verification
	[Value]
		[Bool] true
	[Key] com.apple.private.tcc.manager.access.delete
	[Value]
		[Array]
			[String] kTCCServiceAll
	[Key] com.apple.rootless.install.heritable
	[Value]
		[Bool] true

Useful for a quick read, but not the actual plist. To get the plist in its canonical XML form — which is what we want, both for accuracy and for citing — we ask explicitly with –xml and clean it up through xmllint:

gabriel@psychopompos Desktop % codesign -d --entitlements - --xml /System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/Resources/system_installd 2>/dev/null | xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.private.apfs.create-synthetic-symlink-folder</key>
    <true/>
    <key>com.apple.private.launchservices.cansetapplicationstrusted</key>
    <true/>
    <key>com.apple.private.package_script_service.allow</key>
    <true/>
    <key>com.apple.private.responsibility.set-arbitrary</key>
    <true/>
    <key>com.apple.private.responsibility.set-hosted-properties</key>
    <true/>
    <key>com.apple.private.security.storage-exempt.heritable</key>
    <true/>
    <key>com.apple.private.security.syspolicy.package-installation</key>
    <true/>
    <key>com.apple.private.security.syspolicy.package-verification</key>
    <true/>
    <key>com.apple.private.tcc.manager.access.delete</key>
    <array>
      <string>kTCCServiceAll</string>
    </array>
    <key>com.apple.rootless.install.heritable</key>
    <true/>
  </dict>
</plist>

The structure is quite easy to conceptualise: the property is a reverse-DNS-style string, drawn from a namespace controlled by Apple. This can assume atomic values (booleans, integers, strings) or dictionaries.

By the way - a third-party developer can’t invent com.acme.rootless.install.heritable and hope the kernel will honour it. The kernel only trusts namespaces it knows about, and those namespaces are all Apple’s.

Entitlements are incorporated into the binary’s signature. If entitlements lived anywhere root could write (a config file, an xattr, a database…) a compromised root could just edit them. By living inside the signature, modification requires re-signing the binary, which requires Apple’s private key, which root doesn’t have.

That covers one half of the problem.

The second half? The first thing I wanted to do when I saw this mechanism was: I create my binary, I self-sign it, and give it all the entitlements I want. Again, you Puny Penguin Parasite, welcome to a secure world - a self-signed object, even if root signed it, is something the kernel sees as signed by an unknown entity. The signature isn’t just any signature — it must chain back to Apple’s root certificate, which the system knows about and cannot be replaced. So root can’t even forge a new signed binary with arbitrary entitlements: the kernel would refuse to honour entitlements from an unknown signer.

The kernel uses entitlements in the following way:

  1. A process invokes a syscall. To fix the ideas, let it be write, requested on a SIP-restricted path.
  2. The VFS layer of the kernel intercepts the syscall.
  3. Before declining, the kernel inspects the calling process’s binary for signed entitlements.
  4. If the signature is valid, chains back to Apple, and contains an entitlement that authorises the operation (in our case, com.apple.rootless.install), the write is allowed through.
  5. Otherwise, the operation is denied.

It should be clear by now that an entitlement is not a bit that is somehow set at runtime, but rather an intrinsic property of the binary that gets verified by the kernel every time it becomes relevant; therefore a binary cannot “gain” entitlements. It cannot ask for entitlement, the entitlement is present because it is declared in the process code and signed by Apple. Or it has not that entitlement. Black or white.

It is useful to classify the entitlements based on their nature. We have public entitlements - generally structured as com.apple.security.something. Fully documented, widely available to developers, these are the entitlements that we use when writing a regular macOS app: com.apple.security.app-sandbox to sandbox your application or com.apple.security.network.client for the network access are pertinent examples. Then we have private entitlements, structured as com.apple.private.something. These are internally documented by Apple, but not available to the public. Only binaries signed by Apple can have them. If you want to try SIP-bypass, you should definitely look into these. Finally, restricted entitlements are a specific subset of private entitlements that not only require a signature from Apple, but also the binary must live in a SIP-protected environment. An example? The binary has the entitlement and is located in /System (e.g. - com.apple.rootless.install.heritable). Again, observe the layered defence pattern: in this case, three factors must converge (signature, position, path integrity).

Here the prostrated penguinist would start screaming “yeeesss, but we have them in linux as well! POSIX Capabilities!!! Apple steals!!! Your daddy is rich! Hasta la victoria siempre!”. No, my little squawking spheniscidae, you wish. There are substantial differences between entitlements and POSIX capabilities (CAP_SYS_ADMINCAP_NET_BIND_SERVICE, …): Linux capabilities are mutable: they can be attached to binaries with setcap, granted or dropped from a running process with capset or prctl, and root can grant or revoke them at will.

Apple’s model is not “stricter” — it’s a different model entirely. Capabilities ask: who currently holds the keys? Entitlements ask: who was authorised by Apple to hold them, forever, at sign-time? The two questions live in different temporal dimensions.

So, to finalise the bigger picture, it is worth mentioning some entitlements. Here I grouped them by what they allow you to do. The list is not exhaustive, btw.

Writing in protected path

The most famous is com.apple.rootless.install. It’s the base entitlement and allows the process to write in restricted paths during the execution. It can be found in some Apple core processes, such as installdsystem_installdsoftwareupdatedmobileassetd, and in some MDM helpers.

Then we have com.apple.rootless.install.heritable, a variant of the former one, that can be inherited by spawned processes. For instance, would an entitled binary launch a shell script, then the child process would still maintain the SIP-bypass entitlement. By the way, this was how Shrootless (with payload in /etc/zshenv, executed by zsh spawned during the post-install phase, inheriting the entitlement from system_installd.) and Migraine (with migration helpers launched by systemmigrationd) worked. Inheritance is where things go wrong.

Accessing protected task ports

com.apple.system-task-port allows obtaining the task port of other protected processes. Even those with hardened runtime. I found it mainly in Apple debuggers, and some crash reporting tools: lldb being a noticeable example. It is the mechanism that allows the system to investigate processes that would be invisible to a third party debugger.

The inverse direction is com.apple.security.get-task-allow. A binary carrying this entitlement declares itself debuggable: it tells the kernel that other processes are allowed to obtain its task port. You don’t put it on the debugger — you put it on the binary you want to debug. It is found in development builds (Xcode adds it automatically to debug configurations) and is absent from shipped Apple production binaries. If you ever wonder why you can lldb your own Swift app but not Finder, that’s the reason.

Skipping sandbox

Some Apple binaries are exempted from the sandbox altogether — typically system daemons that operate across multiple user contexts and would be unworkable inside a sandbox container.

The exemptions are granted through private entitlements under com.apple.private.security.*. The exact names change between macOS versions and not all of them are documented, so rather than chasing a moving target, the easier way to spot them is to inspect a daemon you suspect:

codesign -d --entitlements - --xml /usr/sbin/some_daemon 2>/dev/null | xmllint --format -

and look for keys in that namespace. Not to be confused with com.apple.security.app-sandbox, which is the opposite: that one declares “I want to be sandboxed” and is what regular third-party apps carry.

Skipping Apple Mobile File Integrity (AMFI)

AMFI (Apple Mobile File Integrity) is the kernel extension that enforces code signing on macOS: it decides whether a binary is allowed to execute based on its signature. Skipping AMFI means executing code that wouldn’t normally pass signature verification.

com.apple.private.amfi.can-load-cdhash allows to load code whose cdhash is in an explicit list. It is used by some specific system services that need to load signed code dinamically.

Another entitlement that allows the process to skip AMFI is com.apple.private.allow-bridgeos-internal-rpc, specific to communications with bridgeOS on Apple Silicon machines.

Modifying NVRAM

Here the most noticeable entitlement is com.apple.private.iokit.nvram-csr that allows to write contents into NVRAM SIP-protected variables. It is also the reason why user-space processes cannot touch boot-args if SIP is active (as opposed to some Apple binaries).

Out of this list, the entitlements that have generated the most CVEs are the ones in the first group because they let processes write into the system itself, which is where attacks tend to converge. The others are less famous but no less powerful: a compromise of AMFI-bypass entitlements would mean executing arbitrary unsigned code, which is the foundation of every malware capability that exists.

Wrapping up

We started this post saying that SIP is not about castrating root. By now you should see why. SIP is a structural answer to a structural problem: in a world where users routinely hand their password to anything that asks politely, “root can do everything” stopped being a guarantee and became a liability. Apple’s answer was not to remove root — that would break Unix compatibility, and probably break my fingers writing about it — but to introduce an authority above it: a kernel policy, anchored to Apple’s signing keys, that no amount of root can override from within the running system.

That authority is delegated, not eliminated. SIP carves exceptions for the Apple binaries that genuinely need to write into /System — installers, updaters, migration helpers — and grants them through signed entitlements. The delegation is what makes the system workable. It’s also what gets exploited: every documented SIP bypass to date has come from convincing an already-entitled process to do something on the attacker’s behalf, not from breaking SIP itself. Shrootless. Migraine. The pattern repeats. It is the structural debt of any delegated-authority model.

And SIP is only one of the layers. We saw last time that SSV protects the system volume cryptographically, independently from SIP. We saw earlier in this post that the kernel’s runtime view can veto even what the signed LocalPolicy claims, and that hardware identity can veto the kernel. Each layer answers a different question, was introduced in a different decade, and constrains the layers above it without knowing about them.

There’s plenty more to dig into. csrutil has corners we haven’t explored, and there’s an older form of “anti-root” protection that long predates SIP and still quietly does its job underneath — BSD file flags. schg, sunlnk, restricted sitting on the same inode, three mechanisms from three eras converging on the same path. I’ll write about those soon. And the rm -rf / on a tweaked VM I promised last time is still on the to-do list — it’ll show up when the surrounding context makes it land properly.

Stay paranoid, and read your entitlements.


LLM Policy

Here you go. At the end of the post. Memento, deinde loquere