
Reverse-Engineering-Notizen und ein eigenständiger PoC für die Access-Cache-Race des macOS-NFS-Clients (CVE-2026-43687), mit Kext-Disassembly-Diff und dtrace-Race-Capture.
Unabhängiges Reverse Engineering der macOS-NFS-Client-Access-Cache-Race (CVE-2026-43687), plus ein funktionierender PoC, der die Race auslöst und sie live mit dtrace aufzeichnet.
Der Bug ist ein unsynchronisierter Lesezugriff auf den nfsnode-Access-Cache-Zeiger
in _nfs_vnop_access. Ein bösartiger NFSv3-Server kann den Access-Cache
dazu bringen, neu zu allokieren, während ein anderer Thread ihn liest, was
eine Kernel-Speicheroff enlegung erzeugt, die ein feindlicher Server beeinflussen kann. macOS 26.7
behebt dies, indem ein lck_rw_t bei nfsnode+0x158 eingefügt und um den Cache-Lesezugriff herum
shared gesperrt wird.
| Feld | Wert |
|---|---|
| CVE | CVE-2026-43687 |
| Komponente | com.apple.filesystems.nfs (_nfs_vnop_access) |
| Betroffen | macOS Tahoe 26.6 und früher, iOS 26.x und früher |
| Behoben in | macOS Tahoe 26.7, macOS Golden Gate 27, iOS 26.7, iOS 27 |
| Auswirkung laut Advisory | "Connecting to a malicious NFS server may disclose kernel memory." |
| CVSS v3.1 | 6.5 (Medium) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| Gemeldet von | R4mbb von KRsecurity und Peter Malone (laut Apples Advisory) |
Apples Advisory für CVE-2026-43687 dokumentiert die Auswirkung und die Patch- Version. Es dokumentiert nicht den technischen Mechanismus:
nfsnode der Race unterliegtaccess(2) statt stat(2) verwenden mussZum Zeitpunkt des Verfassens wurde kein öffentliches technisches Writeup gefunden. Dieses Repository füllt diese Lücke mit einer unabhängigen Reverse-Engineering- Analyse des NFS-Kexts zwischen 26.6 und 26.7 sowie einem funktionierenden PoC, der die Race auf einem Live-Ziel reproduziert.
Dies ist keine Entdeckungs-Behauptung. Die CVE wurde von R4mbb und Peter Malone gemeldet und von Apple gepatcht. Der Beitrag hier ist die technische Analyse und die Reproduktion.
_nfs_vnop_access im NFS-Client liest nfsnode+0x158 — den Zeiger
auf das per-UID-Access-Cache-Array — zweimal innerhalb eines einzigen Aufrufs,
ohne eine Sperre zu halten:
; macOS 26.6, com.apple.filesystems.nfs
fffffe000b52def4 ldr w8, [x20, #0x160] ; count
fffffe000b52def8 cmp w23, w8
fffffe000b52defc b.ge ...
fffffe000b52df00 ldr x8, [x20, #0x158] ; cache ptr (read #1)
...
fffffe000b52df98 ldr x8, [x20, #0x158] ; cache ptr (read #2)
fffffe000b52dfb0 ldr w21, [x9] ; dereference
Der Writer, _nfs_nget, reallokiert das Array mit kalloc_data und
speichert das Ergebnis bei +0x158, wann immer der Client eine neue UID vom
Server sieht:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
Wenn ein anderer Thread _nfs_nget auf demselben nfsnode zwischen den
beiden Ladevorgängen des Readers erreicht, liefert der zweite Ladevorgang den neuen Zeiger, während
der erste Lesezugriff — bereits zur Berechnung eines Offsets verwendet — auf dem alten
basierte. Die anschließende Dereferenzierung liest aus freigegebenem Kernel-Heap.
Der 26.7-Fix:
fffffe000b9e3064 str x0, [x22, #0x168] ; cache ptr moved
fffffe000b9e306c str w28, [x22, #0x170] ; cache count moved
fffffe000b9e307c add x0, x22, #0x158 ; lock slot
fffffe000b9e3084 bl _lck_rw_init ; init RW lock
und in _nfs_vnop_access:
fffffe000b9f0200 add x0, x20, #0x158
fffffe000b9f0204 bl _lck_rw_lock_shared ; take the lock
fffffe000b9f0208 ldr x9, [x20, #0x168] ; cache pointer
...
fffffe000b9f0230 bl _lck_rw_unlock_shared ; release the lock
Änderung des Struct-Layouts:
| Offset | macOS 26.6 | macOS 26.7 |
|---|---|---|
+0x158 | cache array pointer | lck_rw_t |
+0x160 | cache count | (part of lock) |
+0x168 | (other) | cache array pointer |
+0x170 | (other) | cache count |
_nfs_nget und _nfs_vnop_access zwischen den
26.6- und 26.7-NFS-Kexts — siehe
docs/PATCH_DIFF.mdnfsnode+0x158 in 26.6lck_rw_t bei +0x158 eingefügt, Cache-
Zeiger nach +0x168 verschoben, lck_rw_lock_shared um den Lesezugriff herum genommenaccess(2) gelangt in _nfs_vnop_access; stat(2)
geht durch _nfs_getattr und erreicht die verwundbare Funktion nieSiehe docs/ANALYSIS.md für das vollständige Writeup und
docs/ARTIFACTS.md für Adressen und Log-Beispiele.
poc.sh — ein ein-Datei-, selbstenthaltener PoC:
nfsd, um Port 2049 freizugeben127.0.0.1, der die
gemeldete UID in jeder Antwort rotiertnoacaccess(2) über test -r /
test -w ausgibtnfs_vnop_access an und zeichnet jeden Aufruf auf,
bei dem sich nfsnode+0x158 mitten im Aufruf ändert_nfs_vnop_access, der beobachtet, wie das Array
sich während eines einzigen Aufrufs ändertDie Race ist die Vorbedingung für die Offenlegung. Um die Race in
ein tatsächliches Leck zu verwandeln, müsste ein Angreifer beobachten, wie der Reader den
veralteten Zeiger verwendet, und diese Bytes irgendwohin propagieren, wo er sie lesen kann. Auf
arm64e ist der Wert bei nfsnode+0x158 PAC-signiert und der per-Boot-Schlüssel
ist von Userland aus nicht verfügbar, sodass dtrace allein die Race beobachten
kann, aber den Zeiger nicht dekodieren kann. Siehe den Abschnitt "Paths tested and ruled out"
in docs/ANALYSIS.md.
Die demonstrierte Auswirkung ist die Race selbst — genau das Fenster, das der 26.7-RW-Lock-Fix schließt.
dtrace verfügbar (kann bei einigen Installationen eine SIP-Anpassung erfordern)python3, dscl, mountDer PoC läuft vollständig auf dem Ziel. Der bösartige NFS-Server bindet an
127.0.0.1 und der Mount erfolgt über Loopback. Dies hält den PoC
selbstenthaltend und reproduzierbar ohne Netzwerkkonfiguration.
chmod +x poc.sh
sudo ./poc.sh
Optionales Tuning über Umgebungsvariablen:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh