What do the Hardened Runtime flag and its exception entitlements change, and what can you inspect about a signature without a Developer ID?
Predict first
Does a MAP_JIT mapping succeed in plain, hardened, and hardened-with-allow-jit builds?
// === mm.swift ===
import Foundation
let p = mmap(nil, 16384, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0)
print(p == MAP_FAILED ? "denied" : "ok")
// === jit.entitlements ===
<?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.cs.allow-jit</key><true/></dict></plist>
- plain: ok | hardened: denied | hardened+allow-jit: ok, flags=0x10002(adhoc,runtime)
- all ok
- all denied
Mental model. Hardened Runtime is a building whose windows are locked by default; each entitlement is a key you have to ask the building manager for in writing.
Signing with -o runtime sets the Hardened Runtime flag, which codesign -dvv shows as flags=0x10002(adhoc,runtime) (0x2 is “adhoc”, 0x10000 is “runtime”). With it on, the process loses permission for things injection attacks use: unsigned executable memory, loading libraries signed by someone else, DYLD_* environment variables, attaching a debugger. The check makes it concrete with an executable mapping created with MAP_JIT: the plain ad hoc build succeeds, the hardened build is denied, and the same hardened build with com.apple.security.cs.allow-jit succeeds. The other exceptions are allow-unsigned-executable-memory, disable-library-validation (for plug-in hosts), and allow-dyld-environment-variables; each widens the attack surface, so ask for the minimum.
What you can check locally: codesign --verify --strict and codesign -d --entitlements - (what the binary claims), spctl -a -t exec -vv, which rejects an ad hoc app (Gatekeeper needs a Developer ID signature plus a notarization ticket), and stapler validate, which reports that no ticket is stapled. What you cannot check without a paid account is the notary service itself: upload with xcrun notarytool submit, wait for “Accepted”, then stapler staple so the ticket travels with the app and Gatekeeper works offline. Not run here: the notarization round trip and Developer ID signing. In CI keep entitlements in a file reviewed like code, because a missing runtime flag or a leftover debugging entitlement is only noticed at notarization.
Hardened Runtime denies JIT memory until the allow-jit entitlement is added.
Common traps
- Shipping with
get-task-allowin the release entitlements. It is a debugging entitlement and notarization rejects it. - Adding every exception entitlement “to be safe”. Each one removes a protection; add only what a test shows you need.
They might follow up with
- Does ad hoc signing satisfy Gatekeeper? No;
spctlrejects it. - Why staple? So the ticket is available offline.
Related questions
- 20.3.2 What does a Mac app need that an iOS app does not: the menu bar, windows, the App Sandbox, Hardened Runtime…
- 20.8.1 What does the App Sandbox actually change in a running Mac process, and how do you test it without a paid…
← What does the App Sandbox actually change in a running Mac process… What is the difference between a stored path and a bookmark, and how… →