For the number of years I’ve been in the macOS community, one fact has always stayed consistent: Developers and users don’t understand what System Integrity Protection really is. Thus in today’s blog post, I want to clear up some misconceptions about this setting in macOS and propose better ways for developers to manage this setting.

Terminology Used:

  • macOS Kernel is known as XNU (X is not Unix).
  • boot.efi is macOS’s boot loader on Intel systems, which loads the XNU kernel.
  • NVRAM is a type of low-level storage on a motherboard separate from the operating system drive, primarily used to store global environment variables that are retained across reboots and drive swaps. Commonly used by Apple for boot.efi and XNU configuration.

Table of Contents:

What is System Integrity Protection

System Integrity Protection, generally abbreviated as SIP, is an OS-level setting in macOS that controls many security aspects. Introduced in OS X 10.11, El Capitan, the goal of this setting was to reduce the abuse seen with root level access, namely protected task tracking, arbitrary driver loading and protected filesystem edits. Instead, users are required to manually reboot into macOS’s recovery environment and disable SIP before performing sensitive tasks in the OS.

SIP sits on the kernel level, specifically handled by XNU’s Configurable Security Restriction stack (abbreviated as CSR). Configuration is read either via the csr-active-config NVRAM variable on Intel-based systems, or via lp-sip0 entry in the Device Tree on Apple Silicon-based systems.

  • Interesting notes:
    • On Intel-based systems, XNU doesn’t read csr-active-config directly. Instead boot.efi detects the value, and passes it to the kernel.
      • Due to this, boot.efi will actually strip bit 0x10 (AppleInternal) on production level machines before passing it on.
    • On Apple Silicon-based systems, XNU directly reads the SIP configuration from the booted Device Tree, allowing for per-volume SIP configuration.
      • Primary benefit for this is allowing special development OS installs, while keeping important data on the SIP protected install
      • Compare this to Intel, 1 SIP value is used for all macOS installs booted.

The raw SIP value is a UINT32 integer, which is treated as a bit field by the Operating System.

  • But what is a bit field?
    • A bit field is a single value that distinctly assigns meaning to bit ranges. In case of the SIP bit field, every one of the 32 bits has a distinct boolean meaning

How XNU uses this bit field is to determine what privileged actions can be used in the OS. The following is a breakdown of valid SIP options in macOS 13.0, found in csr.h:

  • A very common misconception with SIP is that it is a simple on or off state, however SIP is actually a cumulation of settings used by the OS.
CSR_ALLOW_UNTRUSTED_KEXTS            = 0x1
CSR_ALLOW_UNRESTRICTED_FS            = 0x2
CSR_ALLOW_TASK_FOR_PID               = 0x4
CSR_ALLOW_KERNEL_DEBUGGER            = 0x8
CSR_ALLOW_APPLE_INTERNAL             = 0x10
CSR_ALLOW_UNRESTRICTED_DTRACE        = 0x20 // formally known as CSR_ALLOW_DESTRUCTIVE_DTRACE
CSR_ALLOW_UNRESTRICTED_NVRAM         = 0x40
CSR_ALLOW_DEVICE_CONFIGURATION       = 0x80
CSR_ALLOW_ANY_RECOVERY_OS            = 0x100
CSR_ALLOW_UNAPPROVED_KEXTS           = 0x200
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE = 0x400
CSR_ALLOW_UNAUTHENTICATED_ROOT       = 0x800

An example of this bit field can be seen in OpenCore Legacy Patcher’s GUI, which has a breakdown of SIP settings, and how to calculate the correct bit field. For example, 0x803 breaks down into 0x1, 0x2 and 0x800:

SIP Breakdown

Now let’s try to break down each setting and give a brief description:

  • CSR_ALLOW_UNTRUSTED_KEXTS
    • Introduction: 10.11 - El Capitan
    • Description: Allows unsigned kernel drivers to be installed and loaded
  • CSR_ALLOW_UNRESTRICTED_FS
    • Introduction: 10.11 - El Capitan
    • Description: Allows unrestricted file system access
  • CSR_ALLOW_TASK_FOR_PID
    • Introduction: 10.11 - El Capitan
    • Description: Allows tracking processes based off a provided process ID
  • CSR_ALLOW_KERNEL_DEBUGGER
    • Introduction: 10.11 - El Capitan
    • Description: Allows attaching low level kernel debugger to system
  • CSR_ALLOW_APPLE_INTERNAL
    • Introduction: 10.11 - El Capitan
    • Description: Allows Apple Internal feature set (primarily for Apple development devices)
  • CSR_ALLOW_UNRESTRICTED_DTRACE
    • Introduction: 10.11 - El Capitan
    • Description: Allows unrestricted dtrace usage
  • CSR_ALLOW_UNRESTRICTED_NVRAM
    • Introduction: 10.11 - El Capitan
    • Description: Allows unrestricted NVRAM write
  • CSR_ALLOW_DEVICE_CONFIGURATION
    • Introduction: 10.11 - El Capitan
    • Description: Allows custom device trees (primarily for iOS devices)
      • Note: This is based off speculation, currently little public info on what uses this bit provides
  • CSR_ALLOW_ANY_RECOVERY_OS
    • Introduction: 10.12 - Sierra
    • Description: Skip BaseSystem Verification, primarily for custom recoveryOS images
  • CSR_ALLOW_UNAPPROVED_KEXTS
    • Introduction: 10.13 - High Sierra
    • Description: Allows unapproved kernel driver installation/loading
      • Note: Current use-case unknown
  • CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE
    • Introduction: 10.14 - Mojave
    • Description: Allows override of executable policy
      • Note: Current use-case unknown
  • CSR_ALLOW_UNAUTHENTICATED_ROOT
    • Introduction: 11 - Big Sur
    • Description: Allows custom APFS snapshots to be booted (primarily for modified root volumes)

Misuse of SIP

Now that we know what SIP is and how it works, we’ll now move onto how developers have been misusing this setting and how we can improve the landscape.

Boolean treatment with csrutil

The most common way developers query the SIP status in macOS is through the userspace tool /usr/bin/csrutil. This is by far the worst way to determine SIP status, solely because it assumes SIP is a simple on/off system. csrutil’s biggest flaw is that it has a very small range of what it accepts as “disabled”, which is defined in csr.h:

#define CSR_DISABLE_FLAGS (CSR_ALLOW_UNTRUSTED_KEXTS | \
	                   CSR_ALLOW_UNRESTRICTED_FS | \
	                   CSR_ALLOW_TASK_FOR_PID | \
	                   CSR_ALLOW_KERNEL_DEBUGGER | \
	                   CSR_ALLOW_APPLE_INTERNAL | \
	                   CSR_ALLOW_UNRESTRICTED_DTRACE | \
	                   CSR_ALLOW_UNRESTRICTED_NVRAM)

If a user simply wants to edit the root volume, for example with OpenCore Legacy Patcher, they’ll only want to toggle bits 0x1, 0x2 and 0x800. However with csrutil’s logic, you’ve entered an “unknown” configuration:

If we look to some common projects checking SIP status, we’ll see the software will fail to recognize a system as “patchable” since there’s no System Integrity Protection status: disabled string in the output.

Hard coding SIP values

Another common issue we’ll see is developers hard coding SIP’s value. For example, SIP would be considered “disabled” with the value of 0xFF in El Capitan. However with Sierra, Apple added CSR_ALLOW_ANY_RECOVERY_OS which shifted SIP to 0x1FF. And this goes on and on as new OS updates add new SIP settings.

Where this becomes an issue is developers assuming SIP is only valid with 0xFF for the bits they require. But if a user updated macOS and got a new SIP bit, suddenly the software’s hard coded assumption is now incorrect.

Reliance on NVRAM

The final major issue is developer’s reliance on NVRAM querying for SIP. While conceptually this is fine, practically this can end up in some noteworthy edge cases:

  1. Running nvram -c clears the hardware’s NVRAM variables, yet XNU still has an active SIP value.
  2. boot.efi and XNU can and will strip SIP bits it deems “unsupported” (ex. 0x10 (AppleInternal) being stripped on production systems).
  3. NVRAM is unused for SIP on Apple Silicon systems (as mentioned earlier, stored in Device Tree under lp-sip0).

How to properly query SIP

Now that we’ve gone over what SIP is and how it’s been misused, we’ll now discuss how developers can improve this.

The main key take aways I want everyone to remember:

  • System Integrity Protection is not a single setting, but instead a cumulation of multiple.
  • Each setting has it’s own uses, avoid having unused bits required for your software checks.
  • Check in with the kernel for SIP status, not generic variables and utilities.

With this in mind, let me introduce you to XNU’s exposed syscalls for SIP status:

int
csr_get_active_config(csr_config_t *config)
{
	return __csrctl(CSR_SYSCALL_GET_ACTIVE_CONFIG, config, sizeof(csr_config_t));
}

This snippet of code gives us the ability to directly query what XNU is actively using as their SIP value.

Now how does one implement this cleanly?

Below I’ll provide 2 examples, one in Objective-C and another in Python through a simple module:

Objective-C implementation

With Objective-C, you’ll simply need to load the libSystem.dylib library and query for the csr_get_active_config() entry:

+ (int)getCurrentSip {

    NSString *sip_path = @"/usr/lib/libSystem.dylib";
    NSString *sip_function = @"csr_get_active_config";

    // Get the function pointer
    void *libSystem = dlopen(sip_path.UTF8String, RTLD_LAZY);
    if (!libSystem) {
        NSLog(@"ParseSip: Error loading libSystem.dylib");
        return -1;
    };

    void *csr_get_active_config = dlsym(libSystem, sip_function.UTF8String);
    if (!csr_get_active_config) {
        NSLog(@"ParseSip: Error loading csr_get_active_config");
        return -1;
    };

    // Call the function
    int (*csr_get_active_config_ptr)(uint32_t *) = (int (*)(uint32_t *))csr_get_active_config;
    uint32_t sip_int = 0;
    int err = csr_get_active_config_ptr(&sip_int);
    if (err) {
        NSLog(@"ParseSip: Error calling csr_get_active_config");
        return -1;
    };

    dlclose(libSystem);

    NSLog(@"ParseSip: Current SIP value: %d", sip_int);
    kDetectedSIPValue = [NSNumber numberWithInt:sip_int];
    return sip_int;
}

Once you’ve gotten the active SIP value, you can next iterate over the bits and validate against what you require:

+ (BOOL)canRootPatch:(int)sip_int {

    NSLog(@"ParseSip: Checking if SIP value can root patch: %d", sip_int);

    NSNumber *kernel_major_version = [OSDetection getKernelMajorVersion];

    if ([kernel_major_version intValue] == kMacOSBigSur || [kernel_major_version intValue] == kMacOSMonterey || [kernel_major_version intValue] == kMacOSVentura) {
        // - CSR_ALLOW_UNTRUSTED_KEXTS      - 0x1
        // - CSR_ALLOW_UNRESTRICTED_FS      - 0x2
        // - CSR_ALLOW_UNAUTHENTICATED_ROOT - 0x800
        if ((sip_int & 0x2) && (sip_int & 0x800)) {
            if ([kernel_major_version intValue] == kMacOSVentura)) {
                if !(sip_int & 0x1) {
                    NSLog(@"ParseSip: SIP value cannot root patch");
                    return NO;
            	}
            }
            NSLog(@"ParseSip: SIP value can root patch");
            return YES;
        }
    } else if ([kernel_major_version intValue] == kMacOSMojave || [kernel_major_version intValue] == kMacOSCatalina) {
        // - CSR_ALLOW_UNTRUSTED_KEXTS  - 0x1
        // - CSR_ALLOW_UNRESTRICTED_FS  - 0x2
        if ((sip_int & 0x1) && (sip_int & 0x2)) {
            NSLog(@"ParseSip: SIP value can root patch");
            return YES;
        }
    }

    NSLog(@"ParseSip: SIP value cannot root patch");
    return NO;
}

Python implementation

The logic for Python is actually quite similar to the Objective-C approach, however I wanted to make a more “out of the box” solution that developers could easily integrate into their projects.

Thus I developed the py_sip_xnu module, where you can easily import the library and invoke get_sip_status(). This returns an SIP object that you can easily detect SIP’s value, a breakdown of each bit as well as a simple booleans for common operations such as root volume editing.

  • Project can also be found on PyPI, and easily installed via pip3 install py_sip_xnu.
import py_sip_xnu

sip_config = py_sip_xnu.SipXnu().get_sip_status()

'''
sip_config = {
    'value': 0,
    'breakdown': {
        'csr_allow_untrusted_kexts': False,
        'csr_allow_unrestricted_fs': False,
        'csr_allow_task_for_pid': False,
        'csr_allow_kernel_debugger': False,
        'csr_allow_apple_internal': False,
        'csr_allow_unrestricted_dtrace': False,
        'csr_allow_unrestricted_nvram': False,
        'csr_allow_device_configuration': False,
        'csr_allow_any_recovery_os': False,
        'csr_allow_unapproved_kexts': False,
        'csr_allow_executable_policy_override': False,
        'csr_allow_unauthenticated_root': False
    },
    'can_edit_root': False,
    'can_write_nvram': False,
    'can_load_arbitrary_kexts': False
}
'''

However for those wanting to manually implement this, the process is fairly straight forward.

To query the raw SIP value:

def __detect_sip_status(self):
    # https://github.com/khronokernel/py_sip_xnu/blob/1.0.3/py_sip_xnu.py#L209-L235

    if self.xnu_major < self._XnuOsVersion.OS_EL_CAPITAN:
        # Assume unrestricted
        return 65535

    libsys = CDLL(self.lib_system_path)
    result = c_uint(0)
    error = libsys.csr_get_active_config(byref(result))

    if error != 0:
        raise Exception("Error while detecting SIP status: %d" % error)

    print("csr_active_config: %d" % result.value)

    return result.value

Once you’ve gotten the raw SIP value, simply use the bitwise & operator to check against important bits:


def __sip_can_edit_root(self):
    # https://github.com/khronokernel/py_sip_xnu/blob/1.0.3/py_sip_xnu.py#L237-L252
    # 0x2   - CSR_ALLOW_UNRESTRICTED_FS
    # 0x800 - CSR_ALLOW_UNAUTHENTICATED_ROOT

    if self.sip_status & 0x2:
        if self.xnu_major < self._XnuOsVersion.OS_BIG_SUR:
            return True

        if self.sip_status & 0x800:
            return True

    return False

Above logic based off of pudquick’s concept.