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.

PID CHAIN connectionprocessIdentifier SecCode from PIDa number, not a process CheckValidityagainst requirement ALLOWwrong binary caller execs a decoysame PID, new image AUDIT TOKEN CHAIN connectionaudit token SecCode from tokenkernel-held identity CheckValidityagainst requirement DENYrace has no effect
The two chains differ in one link. A PID names a slot that the caller can refill; an audit token names the peer the kernel is actually talking to.

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_pidpathkSecGuestAttributePidSecCodeCopyGuestWithAttributesSecStaticCodeCheckValidity. 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.

Sources & freshness

How current is this note?

Sources checkedAugust 31, 2026

The latest source-review, content-update, or publication date is shown.

ReviewPublic sources reviewed

Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.