Escaping the Apple Sandbox by Spawning an Executable
What are spawn attributes?
Yet another quick one here. Late last year, I found and reported a remarkably simple sandbox escape in Apple's sandbox. It was patched back in February of this year across all Apple OSes in version 26.3 (in addition to the earlier majors that also received updates on that day), though I was only able to reliably exploit it on macOS. The bug was assigned CVE-2026-20628, and all you had to do to exploit it was to spawn an executable. Well... sort of. You did also need to include some spawn attributes when spawning it.
I wrote about spawn attributes in an earlier article of mine. I didn't explain what they were, more generally, in that article so I will here. They are basically what their name implies: they define attributes for the to-be-created child process in a posix_spawn call. And, as referenced in the aforementioned article, Apple includes some private attributes (including one that passes a binary blob to a specified MAC policy).
The Sandbox MAC policy
The Sandbox kernel extension on Apple platforms defines its own MAC policy. This is actually core to its function, as it can define hooks into many operations and direct them through its own sandbox evaluation code. However, it also opens itself up to consuming policy info via the posix_spawnattr_setmacpolicyinfo_np API (as explained in my previous article). The Sandbox MAC policy does indeed include a private API for consuming such info. The binary info blob it accepts defines, mainly, a built-in Sandbox profile name as well as a path for filesystem containerization. The former is really the only relevant part of the blob for the purposes of this bug. But what is a built-in profile?
Built-in profiles
In my DirtyDict article, I briefly discussed sandboxing on Apple platforms as well as sandbox profiles. Built-in profiles are really just sandbox profiles that have been built directly into the Sandbox kernel extension, instead of as external text files. macOS only has a few of them, but one in particular was the key to eventually breaking out of the sandbox. The no-network profile allowed for filesystem access to the user's home folder. With that level of access, it's trivial to escape. As I explained in my DirtyDict article, all one has to do is to modify a shell config file and then open Terminal. But wait, how do we even get from another sandbox to the no-network profile in the first place to then escape? That's where spawning comes into play.
The full chain
As I said at the start, the full chain is remarkably simple. When a sandboxed process launches a child process, that child process usually inherits the parent's sandbox profile. However, I found that, if you defined a built-in profile name using the spawn attributes, that profile was used instead. As an example, if a sandboxed process without access to the user's home directory spawned a child process, and used spawn attributes specifying the no-network profile for the child while doing so, the child would get the no-network profile applied to it instead of the parent's (and would thus gain access to the user's home directory over the parent).
All a sandboxed process would need to do is to spawn a separate executable that performs the shell config attack. It wouldn't even need that executable to be custom. It could just as easily spawn /bin/bash with -c (the flag that allows a user to define the command(s) for Bash to run). Since Bash still ships with macOS, this would be a reliable way to exploit this bug. So what was the fix?
The fix
The fix for this bug was also remarkably simple. Now, already-sandboxed processes cannot define a built-in profile name for child processes. The only exception to this appears to be if that process carries the entitlement com.apple.private.security.sandbox-spawnattrs. Apple could have went for a more complex approach, where policies were compared against each other and where a spawn would only be denied if the built-in profile was more permissive than the parent's profile. However, this approach of blanket denial is much simpler, and, in my honest opinion, the better approach.
Why the delay?
So, why did I delay this write-up? In short, there was a separate bug I found which Apple was still investigating. They asked me to withhold disclosure of both bugs until the investigation of that second bug was complete. Ultimately, they determined it to not be a security vulnerability, giving me permission to disclose both bugs. I may end up writing about this second bug, but it really wasn't as interesting as this one.
To keep an eye out for that, and/or anything else I post, remember to watch this space.