UPDATE 2023.10.10.: After chatting with Thijs Alkemade, @xnyhps, updated the XPC part of the post as I originally misunderstood Apple’s intent.

Apple introduced Launch Constraints in macOS Ventura (13) as a response to some common attack scenarios. LC was probably the most impactful mitigation against various type of vulnerabilities. Before we dwell into LC let’s review a couple of old vulnerabilities, which would have been not exploitable if LC was present.

Old macOS Vulnerabilities Link to heading

TCC Bypass with imagent.app Link to heading

This vulnerability was discovered by Adam Chester (xpn) back in 2019 and documented on his blog post “Bypassing MacOS Privacy Controls”. The idea is that the imagent.app, located at /System/Library/PrivateFrameworks/IMCore.framework/imagent.app had some very interesting entitlements, which allowed it to access the contacts and some entries in the keychain. This is shown below.



Adam found that this application will try to load plugins from imagent.app/Contents/PlugIns, and its code signing properties allowed to load any third party, non Apple signed plugins. The only issue was that the application resided in a SIP protected directory. However nothing prevented us from making a copy of it, for example into /tmp/, create a plugin, and launch it from there.

This way we could load an arbitrary plugin into this powerful application and inherit its entitlements and so bypass TCC and keychain.

TCC Bypass Using Directory Utility.app, CVE-2020-27937 Link to heading

This vulnerability was discovered by Wojciech Reguła (_r3ggi) back in 2020, and it’s similar to the previous one. It was also documented in Wojciech’s blog post, “Change home directory and bypass TCC aka CVE-2020-27937”. Directory Utility had some very powerful entitlement, which granted it rights to change the user’s properties, like HOME folder.


Similarly to imagent, the application’s code signing properties were not restrictive enough, and allowed us injecting code into the application by creating plugins. So we could make a copy again, and inject code into the app. This again allowed us inheriting the utility’s entitlements. By changing the HOME directory, we can plant a new TCC database with our custom rules.

TCC Bypass Using configd, “powerdir” Link to heading

This vulnerability was discovered by Jonathan Bar Or (yo_yo_yo_jbo) back in 2021, and was documented on Microsoft’s security blog under the title, New macOS vulnerability, “powerdir,” could lead to unauthorized user data access. Here the exploitable utility was configd, which is located at /usr/libexec/configd. Among many powerful entitlements it also had one TCC related, namely the following.

[Key] com.apple.private.tcc.allow
		[String] kTCCServiceSystemPolicySysAdminFiles

This allows the tool to change the HOME directory of a user, which ultimately leads to a full TCC bypass as we can plan an arbitrary database. The tool’s code signing properties again allowed injecting a custom bundle. However in this case instead of making a plugin, we could specify the bundle to be loaded by a command line switch. Although this tool is normally launched by launchd upon startup, there was nothing preventing us launching it again through the command line.

Introducing Launch Constraints Link to heading

The above three vulnerabilities, is just a small subset of similar issues that have been found over the years. As we can see there are some common attack patterns, like executing binaries the way they were not expected to be launched or executing them from alternative places. Apple introduced Launch Constraints in macOS Ventura to mitigate these type of exploitation scenarios. It can control, who, how and from where a process can be launched.

Each system binary is assigned to a constraint category, which are defined inside the trust cache. Trust cache is a list of all system binaries, their hashes, and now with version 2, their launch constraint category. The various Launch Constraints categories are defined inside AMFI (AppleMobileFileIntegrity).

Categories are consists of three different parts:

“Self Constraints”, “Parent Constraints” and “Responsible Constraints”. They mean the following. Self Constraints - These are constraints the application itself must meet. Parent Constraints - These are constraints related to the parent process of the application. Responsible Constraints - These are constraints related to the process which is responsible for launching the application.

For example when we invoke an XPC service, the application will be the service, the parent will be launchd and the responsible process will be the one, which invoked the service at first place.

Let’s see it in action. We make a copy of the FindMy.app and try to launch it. It will crash.

csaby@max /tmp % cp -r /System/Applications/FindMy.app .

csaby@max /tmp % open FindMy.app 
The application cannot be opened for an unexpected reason, error=Error Domain=RBSRequestErrorDomain Code=5 "Launch failed." UserInfo={NSLocalizedFailureReason=Launch failed., NSUnderlyingError=0x6000000032d0 {Error Domain=NSPOSIXErrorDomain Code=162 "Unknown error: 162" UserInfo={NSLocalizedDescription=Launchd job spawn failed}}}

The error message is not very descriptive, but if we monitor AMFI logs, we will find the following.

csaby@max /tmp % log stream | grep AMFI
2023-09-19 14:18:21.273482+0200 0x2e3486   Default     0x0                  0      0    kernel: (AppleMobileFileIntegrity) AMFI: Launch Constraint Violation (enforcing), error info: c[1]p[1]m[1]e[2], (Constraint not matched) launching proc[vc: 1 pid: 52468]: /private/tmp/FindMy.app/Contents/MacOS/FindMy, launch type 0, failure proc [vc: 1 pid: 52468]: /private/tmp/FindMy.app/Contents/MacOS/FindMy

We see that we violated Launch Constraints.

Launch Constraints Categories Link to heading

As noted before categories are defined inside AMFI. It was first reversed by Linus Henze, and the initial categories can be found on his Github Gist, Description of the Launch Constraints introduced in iOS 16. Let’s analyze two of them.

Category 1:

     Self Constraint: (on-authorized-authapfs-volume || on-system-volume) && launch-type == 1 && validation-category == 1

     Parent Constraint: is-init-proc

Self constraint refers to the application itself. on-authorized-authapfs-volume || on-system-volume is descriptive: the application must reside either on the System volume or an authorized APFS volume, which likely refers to the cryptex volumes, which are used for RSR (Rapid Security Response) updates. Launch types are documented on Apple’s developer documentation, cs_launch_type_t. Currently (as of macOS 14) there are four different types. 1 refers to a system service, thus this is a binary that should be launched as a service. Validation category 1 means it must present in the trust cache.

The parent constraint refers to the parent, and is-init-proc refers to launchd.

In summary, this is a binary that should reside on the System volume or an authorized APFS volume, and be launched as a system service by launchd.

Let’s examine Category 2.

Category 2:

     Self Constraint: on-authorized-authapfs-volume || on-system-volume

This is less restrictive, it only says that the binary should be on the system volume or an authorized APFS volume.

Let’s find a few binaries that are assigned to these categories.

The Trust Cache Link to heading

The launch policy can be found in multiple places:

/System/Volumes/Preboot/[uuid]/boot/[long hex]/usr/standalone/firmware/FUD/BaseSystemTrustCache.img4
/System/Volumes/Preboot/[uuid]/boot/[long hex]/usr/standalone/firmware/FUD/StaticTrustCache.img4

OSLaunchPolicyData is an IM4P (Image4 Payload) data file, we need to extract the actual content from it. We can use the PyIMG4 Python library for that.

csaby@max /tmp % pyimg4 im4p info -i /System/Library/Security/OSLaunchPolicyData 
Reading /System/Library/Security/OSLaunchPolicyData...
Image4 payload info:
  FourCC: ltrs
  Description: 1
  Data size: 329.14KB
  Encrypted: False

csaby@max /tmp % pyimg4 im4p extract -i /System/Library/Security/OSLaunchPolicyData -o OSLaunchPolicyData.data
Reading /System/Library/Security/OSLaunchPolicyData...
Extracted Image4 payload data to: OSLaunchPolicyData.data

We can also use the other trust caches, but they are IMG4 files, so first we need to extract the IM4 Payload, for which we can also use pyimg4:

pyimg4 img4 extract -i BaseSystemTrustCache.img4 -p BaseSystemTrustCache.im4p
pyimg4 img4 extract -i StaticTrustCache.img4 -p StaticTrustCache.im4p

Once we have the data extracted we can use the trustcache utility to work with the data.

csaby@max /tmp % trustcache_macos_arm64 info OSLaunchPolicyData.data | head   
version = 2
uuid = CCC03EBE-7949-460E-A335-14C6396FC927
entry count = 13713
000600f05b768de957b57afbd576ad031b7dc984 [none] [2] [2]
0008df4d1e0d4b276a82f3e086bea3e93670cf94 [none] [2] [2]
000c95ce2e4f99248a33e5c7f690452f89afc16b [none] [2] [0]
0019e93d101896746f77a3b7047d2d8281352fc5 [none] [2] [3]
0020f949545b505610f55f962520bfb4f1851f1d [none] [2] [2]
002655b46255b6fb2f5b2a4987345cc0e46836f8 [none] [2] [2]
002aa16545d098699c706c27a08e834243f5b984 [none] [2] [2]

The trust cache follows the following structure:

struct trust_cache_entry2 {
	uint8_t cdhash[CS_CDHASH_LEN];
	uint8_t hash_type;
	uint8_t flags;
	uint8_t constraintCategory;
	uint8_t reserved0;
} __attribute__((__packed__));

Thus the 4th column defines the constraint category.

I have a script, which looks through the file system, and looks up each binary against the trust caches, and the categories. For Category 1 we said that it’s a system service, and expected to be launched by launchd only. If we look up files that are indeed in this category we indeed find daemons, like:


For Category 2 we find binaries, that are various system utilities, like:


Based on the analysis we have done, this is all expected.

Reversing Constraints Link to heading

While on macOS 13 we had 7 constraint categories on macOS Sonoma we have 18. As there was no public information on that I decided to reverse it, and currently it’s documented on my GitHub Gist, macOS Sonoma (14) Launch Constraints. But how can we find and reverse the categories.

First, we need to access the AppleMobileFileIntegrity (AMFI) kernel extension, where this information sits. The easiest way is to download Apple’s Kernel Development Kit (KDK), where we will have the binary of the AppleMobileFileIntegrity kernel extension. It defines many symbols starting with the prefix kConstraintCategory. They are sequentially followed by each other and based on the names we can find the category number and if it’s Self or Parent type. Here is a list of the symbols from Sonoma Beta 1 (although they didn’t appear to change in number).


We can dump all of them, and eventually we get a big blob of stream.


This data is DER (ASN.1) encoded stream. There are plenty of tools that can decode DER encoded HEX stream into ASN.1 text representation. For example online: ASN.1 Decoder or the python-asn1 library and its dump.py script, andrivet/python-asn1.

For example the following stream:


Translates to:

  [U] INTEGER: 1
      [U] UTF8STRING: $or
      [C] SEQUENCE
        [U] SEQUENCE
          [U] UTF8STRING: on-authorized-authapfs-volume
          [U] BOOLEAN: True
        [U] SEQUENCE
          [U] UTF8STRING: on-system-volume
          [U] BOOLEAN: True
      [U] UTF8STRING: launch-type
      [U] INTEGER: 1
      [U] UTF8STRING: validation-category
      [U] INTEGER: 1

And this can be reconstructed into:

Self Constraint: (on-authorized-authapfs-volume || on-system-volume) && launch-type == 1 && validation-category == 1

Attack Mitigation Link to heading

Now that we understand Launch Constraints let’s explore why they mitigate the exploits discussed before.

Both the TCC bypass with imagent.app and TCC bypass using Directory Utility.app (CVE-2020-27937) are mitigated by having the (on-authorized-authapfs-volume || on-system-volume) requirement in the executable’s self constraint, which means that if we make a copy of these apps, we won’t be able to execute them. This essentially prevents us to load any plugin into these apps.

“TCC bypass using configd” is mitigated by having the Parent Constraint: is-init-proc in configd’s requirement. This means that we won’t be able to run it from the command line, and load an arbitrary bundle.

Although these vulnerabilities have been fixed, Launch Constraints would have made them unexplainable at the first place.

Although unrelated but the trust cache helps to mitigate common downgrade attacks, when someone used an old and vulnerable binary from an old macOS version on a newer one. As the old one was still properly signed and entitled, it could be used to execute attacks again.

Old Third Party Vulnerabilities Link to heading

Now let’s review some common attacks involving third party software. There are two big groups nowadays, one is exploiting XPC services and the other is injecting code into Electron based applications to bypass TCC.

Global XPC Daemon Attacks Link to heading

One of the main issue with Mach type XPC connections is that by default anyone can talk to them if not sandboxed. As often times there are third party XPC services running as root on the system (they typically come in the form of Privileged Helper Tools) they are often abused.

Both myself and Wojcieh Regula talked and wrote about such attacks in the past, we both have a series of blogposts about the subject:

CVE-2019-20057 - Secure coding XPC services - Part 1 - Why EvenBetterAuthorization is not enough?

Secure coding XPC Services - Part 2 - Checking CS (CodeSigning) flags of the client

CVE-2020-0984 - Secure coding XPC Services - Part 3 - Incorrect client verification

CVE-2020-14978 - Secure coding XPC Services - Part 4 - Improved client authorization

CVE-2020-14977 - Secure coding XPC Services - Part 5 - PID reuse attacks

Learn XPC exploitation - Part 1: Broken cryptography

Learn XPC exploitation - Part 2: Say no to the PID!

Learn XPC exploitation - Part 3: Code injections

The main issue is that the XPC service must ensure that only the real XPC client can connect to it, otherwise others can abuse the service. Often time this is missed, as it’s not trivial to do. Typically all of the following must be ensured:

  1. Code signing verification of the client must happen based on the Audit token
  2. The client’s code signature must ensure that it’s signed by the official developer certificate
  3. To avoid downgrade attacks the client’s version must be checked
  4. To avoid injection it must be verified that the client doesn’t posses any insecure entitlements, like disable-library-valdation.

One can use the NSXPCConnection setCodeSigningRequirement: method for this.

Embedded XPC Service Attacks Link to heading

This type of attack tries to abuse XPC services, which are embedded in the application, and typically not available system wide. The way we can abuse it, is embedding the XPCService in our application bundle, thus it will become part of our process domain, and so we can call it. Alternatively we can use the xpc_add_bundles_for_domain function to add it to our bundle. This might be an intersting attack if the specified service has an entitlement we need.

There are not many attacks exploiting this behaviour, especially on third party software. There has been some attacks on Apple binaries in the past, like CVE-2020-9971 found by Zhipeng Huo, @R3dF09 and documented on Tencent’s blog or CVE-2022-32826 found by Mickey Jin, @patch1t and documented on his blog.

Electron Attacks Link to heading

The Electron framework became very popular as it allows developers to develop to multiple platforms at once, and all the source is HTML and JavaScript based, basically creating a web application as a thick client.

Many people wrote about such attack, here are two posts from Wojciech Regula and Adam Chester.

Abusing Electron apps to bypass macOS’ security controls

MacOS Injection via Third Party Frameworks

The basic idea is that using environment variables like ELECTRON_RUN_AS_NODE allows someone to interact with the application from the command line and run any JS code on behalf of the app, or using the --remote-debugging-port or the --inspect options someone can connect a debugger and again inject custom code.

This is often used to bypass TCC controls, as such apps often have camera, screen capture or similar exceptions.

Launch and Environment Constraints Link to heading

With macOS Sonoma (or in fact at later versions of macOS Ventura) Apple introduced Launch Environment Constraints for third party apps. This means that now anyone can create self, parent and responsible constraints for their apps. Moreover we can also define a “Library Load Constraint”, which allows us defining developers IDs from which our app can load external libraries or bundles. Apple made very good documentation about how to use it, which can be found on their developer website, at Applying launch environment and library constraints and Defining launch environment and library constraints.

Applying it is rather simple. We make a new “coderequirement” property list file in our Xcode project for our target, and in the “Build Settings”, under “Signing”, we can apply it.


Now let’s explore how these constraints (supposed to) mitigate our exploits.

Securing XPC Connection Link to heading

In Apple’s Protect your Mac app with environment constraints WWDC 2023 talk Robert says the following:

Now let’s walk through some process relationships and talk about how you can use launch constraints to secure them. First assume that MyDemo.app is your app. You can set a self constraint on my MyDemo.app to require that it launch as an application from Launch Services. When your app requests a connection to your XPC service, launchd spawns the XPC service and is the parent of that XPC service but your app is “responsible” for that XPC service. You could set a responsible process constraint on MyXPCDemo.xpc to indicate that only MyDemo.app should be responsible for it.

This suggests that if we set our main app as the responsible for launching the XPC service makes the service secure. After talking to Thijs I realized that Apple talks about embedded XPC services here and not global daemon type services. In that case setting the responsible process to the main app can likely* mitigate attacks abusing these services.

*Although I’m not sure what happens if we add an XPC bundle to an app using xpc_add_bundles_for_domain, if that bundle is inside another app. Can it belong to two domains? If yes, can we connect to it if it’s already launched? Lot’s of questions to dig into.

If we look at global XPC services the story is different. Responsible process for global XPC services might be meaningless really, as there can be many events starting it (monitored folders, etc…), thus at this point I’m not sure if the following behaviour is intentional or not. At the time of this writing (Sonoma release) the responsible process for the daemon XPC service is the XPC service itself instead of the connecting client. (Submitted FB: FB13206884). Assuming for a second that it’s a bug, we still won’t be able to launch the XPC service in our attacker code, but if it’s active already (maybe because it was invoked by the original app), there is nothing preventing us from connecting to it. So while setting the constraint might be a good idea, and would limit the attack timeframe, it doesn’t solve the main issue, and our XPC service should still properly validate the connecting client. That is still the only way to secure it. Also as mentioned in the beginning it doesn’t even work this way now.

Securing Electron Applications Link to heading

In Apple’s Protect your Mac app with environment constraints WWDC 2023 talk Robert says the following:

Just like in real parent-child relationships, parent processes have a huge amount of influence over how a child behaves. On macOS, the power to posix_spawn another process gives the parent the ability to control nearly all input to the child. The parent process can also limit the child’s access to system resources. This level of control can cause the child to load unexpected code, to run unexpected features, or to behave in ways that make the process more vulnerable to attack.

So the idea would be to limit the parent to control the client. We can eliminate the spawn issue with setting launch-type to “3”, which means that the application has to be opened by LaunchService, with the user clicking on the app. However the open command has the same impact, which allows us to control arguments or environment variables, or we can use the Launch Services API as follows.

NSWorkspaceOpenConfiguration *conf = [NSWorkspaceOpenConfiguration configuration];
conf.environment = @{
[[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:@"/Applications/Electron.app"]

Although it’s not the same as posix_spawn and it’s somewhat more limited, we can still control crucial parts (arguments and environmental variables) of the application. So our Electron app issue is not quite solved yet.

Final thoughts Link to heading

Launch Constraints introduced in macOS Ventura effectively mitigate many logic attacks that were found before, and makes the attacker’s life much harder. I think that it was a huge thing and a very good engineering and design success.

Launch Constraints for third party apps, while useful, and does limit the attack surface, it doesn’t solve it completely and it’s simply not an answer to many of the common attacks. It can easily provide a false sense of security. There are still ways around it, and attackers will still be able to exploit logic vulnerabilities, but in somewhat more limited fashion. It’s a step in a good direction, but we are not there yet. It’s a good idea to start implementing this, as it will be more common, and will become more relevant as time goes on. I hope that Apple will tune and add further restrictions to the constraints available for third parties.