
Reverse engineering notes and a working PoC for the macOS PackageKit symlink-following bug (CVE-2026-28912), with disassembly diff of the 26.6 fix.
Independent reverse engineering of the macOS PackageKit symlink-following bug (CVE-2026-28912), plus a working PoC that demonstrates the installer writing an attacker-controlled file as root through a directory symlink in the install destination path.
The bug is an unsynchronized path walk in PKCoreShove's file-relink logic. A malicious .pkg can declare a destination path in an unprivileged directory, place a directory symlink at one of the path components pointing at a privileged location, and cause the installer — running as root — to write into the symlink target. Apple's 26.6 fix adds _PKSIPOpenPathSafely which walks each path component with O_NOFOLLOW and rejects any symlink.
| Field | Value |
|---|---|
| CVE | CVE-2026-28912 |
| Component | PackageKit (PKCoreShove, PKBundleComponent) |
| Affected | macOS Tahoe 26.5 and earlier |
| Patched in | macOS Tahoe 26.6 |
| Advisory impact | "An app may be able to gain root privileges." |
| CVSS v3.1 | 7.8 (High) |
| Reported by | Original finders per Apple's advisory |
Apple's advisory for CVE-2026-28912 documents the impact and the patched version. It does not document the technical mechanism:
No public technical writeup was found at the time of writing. This repository fills that gap with an independent reverse engineering analysis of PackageKit between 26.4 and 26.6, and a working PoC that demonstrates the primitive live.
This is not a discovery claim. The CVE was reported by the original finders and patched by Apple. The contribution here is the technical analysis and the reproduction.
PKCoreShove _relinkFile:dest: in macOS 26.4 walks the destination path component-by-component when creating a file:
; macOS 26.4, PackageKit
1a9fb14f8 _relinkFile:dest:
...
bl _linkResolutionProhibitted ; returns 0 in the normal case
mov w8, 0x10 ; RENAME_NOFOLLOW_ANY
cmp w0, 0
csel w22, w8, wzr, ne ; w22 = 0x10 if prohibited, else 0
...
mov x2, x22
bl _renamex_np ; uses w22 as flags
_linkResolutionProhibitted returns 0 (false) when the calling process cannot modify SIP files — the normal case for a user-initiated install. That disables RENAME_NOFOLLOW_ANY, so the rename follows any symlink in the destination path.
The 26.6 fix adds _PKSIPOpenPathSafely, called from PKBundleComponent initWithBundleAtPath:relativeToDestination::
; macOS 26.6, PackageKit
1aa6dfe18 bl _PKSIPOpenPathSafely ; walks each component with O_NOFOLLOW
_PKSIPOpenPathSafely:
Any component that is a symlink pointing outside the intended path is rejected with EPERM or ELOOP, and the install fails.
See docs/ANALYSIS.md for the full writeup and docs/ARTIFACTS.md for addresses and log samples.
link.sh — a single-file, self-contained PoC:
The demonstrated impact is the symlink-following primitive — the exact behavior the 26.6 fix closes.
The PoC runs entirely on the target. The symlink and payload are both created locally. This keeps the PoC self-contained and reproducible without any network configuration.
chmod +x link.sh
./link.sh
Optional tuning via environment variables:
sudo CONTENT="test content" ./link.sh
CONTENT sets the content written through the symlink. By default it is hello.
[*] System information:
ProductName: macOS
ProductVersion: 26.4
BuildVersion: 25E246
[*] Setup...
Symlink: /Users/nerd/cve-poc/target → /Users/nerd/cve-poc/real
[*] Build .pkg...
Payload: /Users/nerd/cve-poc/payload/Users/nerd/cve-poc/target/poc.txt
Package: /Users/nerd/cve-poc/poc.pkg
[*] Install (password prompt expected)...
installer: Package name is poc
installer: Installing at base path /
installer: The install was successful.
[✓] VULNERABILITY CONFIRMED
Landed at /Users/nerd/cve-poc/real/poc.txt
Owner: root:wheel
Content: hello
CVE-2026-28912 trigger SUCCESSFUL
On a patched system (26.6), the same PoC fails:
[✗] Not triggered
No file was created in /Users/nerd/cve-poc/real/
See docs/PATCH_DIFF.md for the disassembly comparison.
Two details in the PoC were discovered during development and are documented here so others building similar tooling don't hit them:
The symlink must be at a path component the installer walks. A file-level symlink at the destination leaf is replaced by renamex_np; only a directory symlink in the middle of the path is followed.
The payload path must mirror the destination path. With pkgbuild --install-location /, the payload path is the destination path minus the leading /. If they don't match, the installer never walks through the symlink.
Both are documented in docs/ANALYSIS.md.
CVE-2026-28912 requires the user's password to run the installer as root. That password is for the install, not for the symlink write. The user authorizes writing to $HOME/cve-poc/target/, and the vulnerability redirects the write to whatever the symlink points to. The two paths differ, and the user never sees the difference.