Hardened Runtime and Sandboxing

November 16, 2018

By Jeff Johnson

The relationship between the hardened runtime and sandboxing can be confusing to Mac developers, both because the hardened runtime is new and because it's not well documented by Apple. I'll attempt to explain the relationship here. App sandboxing was introduced in Mac OS X 10.7 Lion and eventually became a requirement for all Mac App Store apps, though developers can also choose to sandbox apps distributed outside the Mac App Store. The hardened runtime was introduced in macOS 10.14 Mojave and is currently optional for all apps, though it is required in order to notarize your app. Apple has announced that at some point in the future, all apps distributed outside the Mac App Store will need to be notarized, which means they will need to be "hardened" too. I suspect that Apple will eventually require Mac App Store apps to hardened as well. This may be surprising to developers, who associate sandboxing with the App Store and the hardened runtime with Developer ID, but the two technologies are independent of the distribution method and independent of each other, which means that a single app can be sandboxed and hardened.

The hardened runtime on Mojave is enforced by System Integrity Protection (SIP). If you boot into the recovery volume, disable SIP in Terminal with csrutil disable, and boot into Mojave again, then the hardened runtime becomes irrelevant and has no effect on apps. [Not entirely accurate, see Addendum.] This is in contrast to sandboxing, which is enforced on sandboxed apps by macOS regardless of whether SIP is enabled. Both the app sandbox and the hardened runtime can be configured in Xcode, under target capabilities. The hardened runtime requires Xcode 10. Both sandboxing and hardening rely on code signing entitlements. For illustration, I'll use my 2 Mac App Store apps, StopTheMadness and Underpass. Go ahead and buy them, I'll wait here. There's even a bundle to buy them both at a discount! After you've installed the apps, the Terminal command codesign --display --entitlements - /Applications/Underpass.app displays the entitlements of Underpass:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
</dict>
</plist>

The com.apple.security.app-sandbox entitlement indicates that sandboxing is enabled. Both Underpass 1.0.5 and StopTheMadness 4.4 are sandboxed, but only Underpass is hardened. There's no special reason for the difference other than I just shipped an Underpass update a few days ago; I'll probably enable the hardened runtime for StopTheMadness too in the next update. Use the following Terminal command to determine whether the hardened runtime is enabled:

$ codesign --display --verbose /Applications/Underpass.app
Executable=/Applications/Underpass.app/Contents/MacOS/Underpass
Identifier=com.underpassapp.underpass
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20200 size=2266 flags=0x10200(kill,runtime) hashes=63+5 location=embedded

The runtime flag indicates the hardened runtime. With StopTheMadness, you just see flags=0x200(kill). Already we see that the hardened runtime is an option even for a sandboxed Mac App Store app.

Sandboxing and hardening are both restrictive technologies. They prevent an app from doing things it ordinarily has permission to do. They're also overlapping protections: the sandbox and the hardened runtime could prevent the same action, so even if the hardened runtime would allow the action, the sandbox may prevent it, and vice versa. Thus, they provide multiple layers of security. We can see this most clearly by looking at Apple Events.

On macOS High Sierra and earlier, a non-sandboxed app is allowed to send Apple Events to any other app. For example:

NSString *source = @"tell application \"iTunes\" to set sound volume to 0";
NSAppleScript *script = [[NSAppleScript alloc] initWithSource:source];
NSDictionary *errorInfo = nil;
if ([script executeAndReturnError:&errorInfo] == nil)	
    NSLog(@"errorInfo:%@", errorInfo);

If the app is sandboxed, however, the AppleScript doesn't work, and an error is returned. To allow the sandboxed app to send Apple Events to iTunes, you need to add a special entitlement:

<key>com.apple.security.temporary-exception.apple-events</key>
<array>
	<string>com.apple.iTunes</string>
</array>

On Mojave, the Apple Event situation has changed dramatically. All third-party apps, and some system apps, now require the user's permission to send Apple Events. Even if an app is neither sandboxed nor hardened, it still requires the user's permission. The first time an app sends an Apple Event, Mojave displays a permission dialog like this:

“MyApp.app“ wants access to control “iTunes.app“. Allowing control will provide access to documents and data in “iTunes.app“, and to perform actions within that app.

If the app is compiled with the 10.14 SDK, then the app's Info.plist file must contain a NSAppleEventsUsageDescription string, which is also displayed in the permission dialog. Without a usage description, the app is not allowed to send Apple Events, and no permission dialog is displayed to the user.

Sandboxed apps on Mojave still require the com.apple.security.temporary-exception.apple-events entitlement. Without that entitlement, sending Apple Events will silently fail, and no permission dialog is displayed, just like on High Sierra. When a sandboxed but non-hardened app has the appropriate com.apple.security.temporary-exception.apple-events entitlement (and NSAppleEventsUsageDescription string with 10.14 SDK), then it is allowed to send Apple Events to the apps specified in the entitlements, and the first Apple Event it sends will trigger a user permission dialog.

By default, apps with the hardened runtime are not allowed to send Apple Events to other apps on Mojave. Again, this will silently fail with no permission dialog. In order to send Apple Events, a hardened app needs the com.apple.security.automation.apple-events entitlement. In Xcode 10, this is added by checking "Apple Events" under Hardened Runtime Resource Access. With this entitlement, a hardened but non-sandboxed app is allowed to send Apple Events to any other app, without having to specify bundle identifiers. The app also needs a NSAppleEventsUsageDescription string, of course, because Xcode 10 uses the 10.14 SDK. And like always on Mojave, the first Apple Event sent will trigger a permission dialog.

What happens on Mojave when an app is sandboxed and hardened at the same time? It's crucial to understand that sandboxing and hardening are both disabling rather than enabling technologies. Without the com.apple.security.automation.apple-events entitlement, a hardened sandboxed app cannot send Apple Events, even if it has com.apple.security.temporary-exception.apple-events sandbox exceptions. and without a com.apple.security.temporary-exception.apple-events entitlement, a hardened sandboxed app cannot send Apple Events, even if it has the com.apple.security.automation.apple-events entitlement. Moreover, the com.apple.security.automation.apple-events entitlement does not give a hardened sandboxed app the ability to send Apple Events to arbitrary targets, because the sandbox still prevents Apple Events to apps other than those specified by com.apple.security.temporary-exception.apple-events. I've tested all of these different situations on Mojave, and you can test them too by building a little sample app with Xcode 10.

We can see in Xcode 10 that the Resource Access section of the Hardened Runtime shows a great deal of overlap with the App Sandbox, while the Runtime Exceptions section has functionality unique to the hardened runtime. What's the reason for the overlap? The sandbox was designed mainly for the App Store, while the hardened runtime was designed mainly for Developer ID. I've just explained in detail how the two technologies can apply to the same app and don't depend on the distribution method, but in the near future the majority of apps will probably use at most one of the two: sandboxing for Mac App Store apps and hardening for notarized Developer ID apps. This is why duplicate entitlements exist.

To some extent, com.apple.security.temporary-exception.apple-events and com.apple.security.automation.apple-events overlap. Other entitlements can overlap to an even greater extent. In a hardened sandboxed app, if you check the Camera exception under App Sandbox, Xcode 10 automatically checks the Camera exception under Hardened Runtime! Now look in your .entitlements file, however. Only one new entitlement has been added: com.apple.security.device.camera. I suspect that since this entitlement is a simple flag, Apple chose to use the same entitlement name for both the sandbox and hardened runtime. A benefit of this approach is that you avoid unexpected problems on Mojave when you enable both the sandbox and hardened runtime in the same app. As we've seen with Apple Events, both the sandbox and the hardened runtime can prevent resource access, and both of them require entitlements to access the resource. Using the same entitlement name for Camera ensures that a hardened sandbox app always has the required entitlements to access the camera. A downside to this approach is that it makes the Xcode 10 interface extremely confusing. The confusion could be mitigated with better documentation. (Just put my blog post on developer.apple.com!) Where the functionality is exactly the same between sandboxing and hardening, the entitlement name can be shared. In cases where the functionality is slightly different, such as with Apple Events, where one entitlement allows arbitrary targets and the other only specified targets, two separate entitlement names were necessary.

I hope I've clarified the relationship between the hardened runtime and sandboxing. For more information about the hardened runtime and debugging, see the blog post I wrote after WWDC.

Addendum

On further testing, my claim that the hardened runtime is enforced by SIP was not entirely accurate. Some protections of the hardened runtime such as debugging and Address Book are indeed enforced by SIP. However, it turns out that the Apple Events protection is not enforced by SIP but rather applies to hardened apps regardless of whether SIP is enabled. I don't yet have a full list of which hardened runtime features are protected by SIP and which aren't.