Notarization: the hardened runtime

In the previous article, I looked at the requirements and features of notarization, drawing attention to the apps which were notarized under legacy rules and therefore don’t meet the same standards. Here I move on to consider one of those requirements in detail: the hardened runtime.

Apple doesn’t appear to have fully explained how the runtime environment of notarised apps is ‘hardened’. The best description it gives to developers is that it “protects the runtime integrity of your software by preventing certain classes of exploits, like code injection, dynamically linked library (DLL) hijacking, and process memory space tampering.” Those types of exploit could be used by an attacker trying to use third-party software for malicious purposes, or as techniques within their own malicious code, of course.

Among the behaviours which are prohibited in a hardened environment are:

  • creating writeable and executable memory, as used in just-in-time (JIT) compilation;
  • code injection using DYLD environment variables, which seems unlikely to be anything other than malicious;
  • loading plug-ins and frameworks signed by other developers, a common practice in apps which support third-party executable extensions;
  • modifying executable code in memory;
  • attachment to other processes, or getting task ports, which is typically required by a debugger.

So the perfect app only runs its own signed code, which is never changed in memory, and all that code has been checked for malicious software. Nothing else should be able to alter that model behaviour.

For many apps, behaving perfectly isn’t a problem, so they can adopt the hardened runtime without any ill-effect on their function. For a small minority, though, its restrictions would kill that app completely. For example, an app featuring its own JIT JavaScript compilation couldn’t do anything without being able to create writeable and executable memory. To address these needs, Apple provides six different opt-outs to features of the hardened runtime. These are expressed in specific entitlements, which are then embedded with the app signature.

These entitlements are:

  •, which allows JIT code;
  •, which allows unsigned executable memory;
  •, which allows DYLD environemnt variables;
  •, which disables library validation;
  •, which disables executable memory protection;
  •, which declares the app to be a debugging tool.

There’s another important entitlement in the context of notarized apps: you should never come across, which enables that app to be run in Xcode’s debug environment. You should also be aware that the entitlement indicating that an app runs in a sandbox is Apps which are both notarized and sandboxed are becoming increasingly common, as they can be distributed through the App Store and in direct sales.

Apps which use the full hardened environment have none of those entitlements. All hardened apps, even those which claim all six opt-outs, show a CodeDirectory flag in their signature of 0x10000(runtime), which is the mark of the hardened app. When hardening is disabled, the flags given for the CodeDirectory are often 0x0(none), but can always include others such as ‘kill’.

Currently, the only method provided by macOS to discover whether an app uses the hardened runtime, and which entitlements it takes, is the codesign command tool, as:
codesign --display --entitlements :- appPath
where appPath gives the app’s full pathname. Thus there is no convenient method to discover what opt-outs a notarized app uses. Some third-party tools do list them: my own Taccy does, together with a detailed account of the other half of the hardened environment, its privacy settings. I’ll look at those in detail in the next article.