Seven programs run as root on this machine that I did not write and Apple did not ship:
com.docker.socket com.vmware.DiskHelper
com.docker.vmnetd com.vmware.IDHelper
com.microsoft.autoupdate.helper com.vmware.MountHelper
dev.orbstack.OrbStack.privhelper
Each one opens a door so that an unprivileged process can ask it to do privileged work. Each one therefore has to answer a question at that door: who is asking? Getting that wrong is the most durable local privilege escalation pattern on macOS.
I set out to check all seven. I found nothing. I was also wrong three times on the way, and each wrong answer was produced by a method that looked reasonable when I chose it. That is the part worth writing down.
The bench came first, and it came from my own mistake
Before pointing anything at other people’s software, I needed to know that my method could tell a sound implementation from an unsound one. The nearest known-positive turned out to be my own lab.
The XPC lab that accompanies my earlier macOS post resolves its peer like this:
let attributes = [kSecGuestAttributePid as String: NSNumber(value: processIdentifier)] as CFDictionary
var code: SecCode?
SecCodeCopyGuestWithAttributes(nil, attributes, [], &code)
That is a PID. The same post says, in its own words, that a PID “is not a durable code identity.” The prose was right and the lab contradicted it.
So I kept it, and wrote the other half: a second server identical in every respect except that it calls
setCodeSigningRequirement, which the platform evaluates against the connection’s audit token.
Under normal use the two servers are indistinguishable. Both admit the approved client and refuse the mismatched one. That already rules out the obvious black-box method: connecting with the wrong identity and seeing whether you are refused tells you nothing, because a sound and an unsound implementation give the same answer to a caller who is being honest.
The difference appears when the caller stops being honest. A client signed under the denied identity
sends its request, then replaces its own process image with a decoy carrying the approved identity, via
posix_spawn with POSIX_SPAWN_SETEXEC. The PID does not change:
scenario client server result
normal denied weak DENY
normal denied strict DENY
PID-reuse race denied weak ALLOW <-- bypass
PID-reuse race denied strict DENY
The weak server logs the bypassed request in exactly the words it uses for a legitimate one:
peer=APPROVED decision=ALLOW gate=code-requirement
effect=BOUNDED input=synthetic
Now the bench can catch something. Time to point it at software I did not write.
False alarm one: the symbols were the wrong question
Docker’s vmnetd looked alarming immediately. Its socket is world-writable, where every other privileged
socket on the machine is not:
srw-rw-rw- root daemon /var/run/com.docker.vmnetd.sock
srw------- root daemon /var/run/VMware Fusion Services.sock
srw------- root daemon /var/run/vpncontrol.sock
And searching its symbol table for peer-credential APIs — LOCAL_PEERCRED, getpeereid, xucred,
audit_token — returned nothing at all. Every Security framework function it imports is TLS: SecTrust*,
SecPolicyCreateSSL, SecCertificate*. Certificate validation for its HTTP client, not authentication of
its socket peer.
A root daemon on a world-writable socket with no visible peer check is a serious-looking shape, and the shape has a name: CVE-2020-15360, “com.docker.vmnetd… allows privilege escalation because of a lack of client verification.”
Which is the moment the finding died. That bug was fixed in 2020, and the socket is still world-writable
in 2026 — so the fix was not to restrict the socket. It was to verify the client in the protocol, where my
method could not see it. vmnetd is a Go binary:
Go build ID: "OFbDS6odAF69FNh4ODnt/0GOyS5Dq2doywSjvz_Jm/EBqhEHMvFWOEVazUyy4C/M1XGNnUdgoBSfWrIOvZg"
Go does not import libc symbols the way I was looking for them. Searching its own symbol table instead:
verifyClient authorize codesign CheckSignature
The verification is there. I had asked “does this binary import the C functions I associate with peer authentication,” and I had read the answer as “does this binary authenticate its peer.” Those are different questions, and only the first one was being answered.
False alarm two: a suspicious call that was not on the decision path
OrbStack’s helper imports xpc_connection_get_pid and — unlike the VMware helpers — imports neither
SecTaskCreateWithAuditToken nor xpc_connection_get_audit_token, and references neither
kSecGuestAttributePid nor kSecGuestAttributeAudit. It does import SecCodeCheckValidity. So it checks
code signatures somehow, and it looks at PIDs, and the usual bridge between those two facts is absent.
The disassembly answers it in five instructions:
sub x2, x29, #0x68 ; &secCode
mov x0, x20 ; the XPC message
bl _SecCodeCreateWithXPCMessage
ldur x20, [x29, #-0x68]
cbz x20, <reject>
SecCodeCreateWithXPCMessage derives the code object from the message itself, which carries the audit
token. There is no PID in the authentication path at all — which is why neither guest-attribute constant
appears. The xpc_connection_get_pid call is real, but its result is packed into a context struct
alongside the euid, the egid and the already-validated SecCode, and handed to the request handler. It is
peer metadata for the operation, not the gate.
The requirements it checks against are pinned rather than broad:
anchor apple generic and identifier "dev.kdrag0n.MacVirt" and certificate leaf[subject.OU] = "HUAQ24HBR6"
Apple’s chain, a specific identifier, and the vendor’s own team. Nothing to report.
False alarm three: the right pattern with the wrong name attached
Microsoft’s AutoUpdate helper was the most convincing of the three, because it contains both patterns.
There is a chain built on proc_pidpath → kSecGuestAttributePid → SecCodeCopyGuestWithAttributes →
SecStaticCodeCheckValidity. That is worse than a PID check: it resolves a PID to a filesystem path and
then validates the file on disk. And there is a second chain using kSecGuestAttributeAudit.
Both exist. Only one can be the door.
The binary is mostly stripped, so the nearest exported symbol is not the containing function and offsets
of +0x13c4 prove nothing. The Objective-C metadata does answer it, after two indirections — the method
lists store selector references, and the pointers are chained fixups that need the image base added
back:
imp 0x10000c4b8 -> isConnectionValidForMAUApp:requestingAppURL:
imp 0x10000c814 -> validateConnection:reason:
The PID chain belongs to a method whose name begins with isConnectionValid. At this point I expected a
finding.
listener:shouldAcceptNewConnection: sits at 0x10000265c. It compares the incoming listener against
four properties, selects a label string, and makes one call whose result goes straight to the accept
branch:
mov x2, x20 ; the connection
mov x3, x23 ; the selected label
bl 0x100011040
mov x22, x0
tbz w22, #0x0, <reject>
And that stub resolves to the other one:
0x100011040: ldr x1, [x1, #0x120] -> 0x10001d120 -> "validateConnection:reason:"
The audit-token method. The requirement it enforces is pinned to Microsoft’s own identifiers, the Apple
Developer ID chain, and team UBF8T346G9. The door is sound.
The sweep, and what it cost to be sure
helper client validation verdict
com.vmware.DiskHelper SecTaskCreateWithAuditToken sound
com.vmware.IDHelper SecTaskCreateWithAuditToken sound
com.vmware.MountHelper SecTaskCreateWithAuditToken sound
com.docker.vmnetd in-protocol verifyClient / CheckSignature sound
dev.orbstack.OrbStack.privhelper SecCodeCreateWithXPCMessage sound
com.microsoft.autoupdate.helper validateConnection:reason: (audit token) sound
com.docker.socket not readable (mode --x--x) unassessed
No findings. Six of seven do it correctly, and the seventh I could not read.
Three methods were tried and three were discarded, in increasing order of cost:
Symbol presence tells you what a binary links, not what it calls. It cannot see verification inside a statically linked runtime, and it cannot distinguish a call on the authorization path from one used for logging.
Black-box probing cannot separate sound from unsound at all here, which the bench proved before the sweep started. Both implementations refuse a dishonest identity and both accept an honest one; only a dishonest caller who is also lying about the PID sees a difference.
Following the call — through the delegate, through the stub, through the selector reference — was the only method that produced an answer I would defend. It is also the only one that is slow.
The three false alarms all share a shape. Each time, a cheap signal produced a suspicion, and the suspicion was about the program when the evidence was only about my measurement. The corrected version of each is not “the helper is fine after all” but “I asked a question that could not distinguish the two cases.”
One thread left open
isConnectionValidForMAUApp:requestingAppURL: exists, resolves a PID to a path, and validates that path
as static code. It is not what guards the connection. But it is compiled in, so something calls it, and if
what it guards is a privileged operation rather than a cosmetic decision, the race is worth revisiting at
that call site rather than at the door.
That is not a finding. It is the one question this sweep raised and did not answer, which is a more useful thing to end with than a clean bill of health.
The bench — both servers, the racer, the decoy, and the expected evidence — is in
labs/macos-xpc-authority/. The weak server is kept deliberately: an instrument that has never been shown
catching something unsound has not earned the right to call anything sound.
How current is this note?
The latest source-review, content-update, or publication date is shown.
Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.
