
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:
_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
HAMMER_COUNT legt die Anzahl der per-User-Hammer-Threads fest (verwendet
die vorhandenen nfsuserNNN-Konten und erstellt sie bei Bedarf).
RUN_SECONDS legt das dtrace-Fenster fest.
[*] ensuring nfsuser accounts exist (UID 201..240)
nfsuser accounts available: 12
[*] starting evil NFS server on 127.0.0.1:2049
[*] mounting /Users/Shared/nfs_test (with noac)
mount check: hello.txt statable
[*] spawning up to 12 per-user threads + 1 root loop
started 12 user threads + 1 root loop
[*] running dtrace for 180s — looking for RACE lines
[*] stopping dtrace
[*] cleaning up
=================== SUMMARY ===================
RACE events caught: 16
Vulnerable interleaving observed — sample:
RACE nd=fffffe5f53cbb940 in=(0,0) out=(b0967e0023297878,0)
RACE nd=fffffe5f534db940 in=(0,0) out=(84dd7e0023297878,0)
RACE nd=fffffe5f53cbb940 in=(0,0) out=(c9a37e0023297878,0)
RACE nd=fffffe5f538ab940 in=(0,0) out=(1867e0023297878,0)
RACE nd=fffffe5f539db940 in=(0,0) out=(149bfe0023297878,0)
CVE-2026-43687 trigger SUCCESSFUL
===============================================
Jede RACE-Zeile ist ein Aufruf von nfs_vnop_access, bei dem sich der Access-Cache-
Zeiger mitten im Aufruf geändert hat.
Auf einem gepatchten System (26.7 / 27) erzeugt dieselbe Workload null RACE-
Events. Siehe docs/PATCH_DIFF.md für den
Disassembly-Vergleich.
Zwei Bugs im Server des PoC wurden während der Entwicklung behoben und sind hier dokumentiert, damit andere, die ähnliche Tooling bauen, nicht darauf stoßen:
ACCESS3resok erfordert post_op_attr, nicht fattr3. RFC 1813
definiert die Antwort als post_op_attr obj_attributes; uint32 access;.
post_op_attr enthält ein bool-Präfix vor dem fattr3. Das Weglassen
dieses bool macht die Antwort 4 Bytes zu kurz; der Client lehnt sie stillschweigend ab
und der Mount wird nie nutzbar.
LOOKUP für AppleDouble-Namen (._*) muss NFS3ERR_NOENT
(2) zurückgeben, nicht NFS3ERR_STALE (70). macOS prüft auf ._<name>-Sidecars
während der normalen Pfadauflösung. STALE zurückzugeben vergiftet den Mount.
Beide sind in docs/ANALYSIS.md dokumentiert.
Die out-Werte in der RACE-Ausgabe haben ein konstantes Low-48-Bit-Muster
(...7e0023297878) über verschiedene nfsnodes hinweg und variieren nur in den hohen
16 Bits. Das bestätigt, dass das Feld PAC-signiert oder obfuskiert ist, kein
roher Kernel-Zeiger. Man kann den Zustandsübergang (NULL →
befüllt) von Userland aus beobachten, aber man kann den Zeiger nicht dekodieren oder dereferenzieren
ohne den per-Boot-PAC-Schlüssel des Kernels.
Aus demselben Grund ist Symbolication gegen das KDK für diese Werte nicht nützlich — es sind keine text-relativen Adressen.
Dieses Repository wird ausschließlich für defensive Sicherheitsforschung und Bildung bereitgestellt.
MIT. Siehe LICENSE.
| 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 |