---
title: "Three False Alarms in a Privileged Helper Sweep"
description: "Seven third-party helpers run as root on this Mac. I checked whether each one authenticates the process talking to it, was wrong three times in a row, and found nothing. The interesting part is why each cheap method failed."
date: 2026-08-30
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/privileged-helper-peer-validation-sweep/
---

Seven programs run as root on this machine that I did not write and Apple did not ship:

```text
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:

```swift
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.

<figure class="diagram">
<svg viewBox="0 0 760 330" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two client validation chains compared. The upper chain takes a process identifier from the connection, resolves a code object from that PID, and checks it against a requirement, leaving a window in which the caller can replace the binary holding that PID. The lower chain derives the code object from the connection's audit token, which the kernel holds and the caller cannot recycle.">
<defs><marker id="pvs-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="18" class="dg-danger">PID CHAIN</text>
<rect x="0" y="36" width="168" height="62" rx="8" class="dg-box" />
<text x="16" y="62" class="dg-label">connection</text><text x="16" y="83" class="dg-muted">processIdentifier</text>
<line x1="170" y1="67" x2="200" y2="67" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="204" y="36" width="184" height="62" rx="8" class="dg-box-danger" />
<text x="220" y="62" class="dg-label">SecCode from PID</text><text x="220" y="83" class="dg-danger">a number, not a process</text>
<line x1="390" y1="67" x2="420" y2="67" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="424" y="36" width="168" height="62" rx="8" class="dg-box" />
<text x="440" y="62" class="dg-label">CheckValidity</text><text x="440" y="83" class="dg-muted">against requirement</text>
<line x1="594" y1="67" x2="624" y2="67" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="628" y="36" width="132" height="62" rx="8" class="dg-box-danger" />
<text x="644" y="62" class="dg-label">ALLOW</text><text x="644" y="83" class="dg-danger">wrong binary</text>
<rect x="204" y="126" width="184" height="52" rx="8" class="dg-box-danger" />
<text x="220" y="150" class="dg-label">caller execs a decoy</text><text x="220" y="169" class="dg-danger">same PID, new image</text>
<line x1="296" y1="124" x2="296" y2="100" class="dg-line dg-dash" marker-end="url(#pvs-a)" />
<line x1="0" y1="206" x2="760" y2="206" class="dg-line dg-dash" />
<text x="0" y="240" class="dg-accent">AUDIT TOKEN CHAIN</text>
<rect x="0" y="256" width="168" height="62" rx="8" class="dg-box" />
<text x="16" y="282" class="dg-label">connection</text><text x="16" y="303" class="dg-muted">audit token</text>
<line x1="170" y1="287" x2="200" y2="287" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="204" y="256" width="184" height="62" rx="8" class="dg-box-accent" />
<text x="220" y="282" class="dg-label">SecCode from token</text><text x="220" y="303" class="dg-accent">kernel-held identity</text>
<line x1="390" y1="287" x2="420" y2="287" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="424" y="256" width="168" height="62" rx="8" class="dg-box" />
<text x="440" y="282" class="dg-label">CheckValidity</text><text x="440" y="303" class="dg-muted">against requirement</text>
<line x1="594" y1="287" x2="624" y2="287" class="dg-line" marker-end="url(#pvs-a)" />
<rect x="628" y="256" width="132" height="62" rx="8" class="dg-box-accent" />
<text x="644" y="282" class="dg-label">DENY</text><text x="644" y="303" class="dg-accent">race has no effect</text>
</svg>
<figcaption>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.</figcaption>
</figure>

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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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

```text
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.