
Notes de rétro-ingénierie et un PoC autonome pour la race de cache d'accès du client NFS de macOS (CVE-2026-43687), avec diff de désassemblage du kext et capture de la race via dtrace.
Rétro-ingénierie indépendante de la condition de course sur le cache d'accès du client NFS de macOS (CVE-2026-43687), accompagnée d'un PoC fonctionnel qui déclenche la course et la capture en direct avec dtrace.
Le bug est une lecture non synchronisée du pointeur du cache d'accès nfsnode dans _nfs_vnop_access. Un serveur NFSv3 malveillant peut forcer le cache d'accès à se réallouer pendant qu'un autre thread le lit, produisant une divulgation de mémoire du noyau qu'un serveur hostile peut influencer. macOS 26.7 corrige cela en insérant un lck_rw_t à nfsnode+0x158 et en le prenant en mode partagé autour de la lecture du cache.
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-43687 |
| Composant | com.apple.filesystems.nfs (_nfs_vnop_access) |
| Affecté | macOS Tahoe 26.6 et antérieurs, iOS 26.x et antérieurs |
| Corrigé dans | macOS Tahoe 26.7, macOS Golden Gate 27, iOS 26.7, iOS 27 |
| Impact selon l'avis | « La connexion à un serveur NFS malveillant peut divulguer de la mémoire du noyau. » |
| CVSS v3.1 | 6.5 (Moyen) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| Signalé par | R4mbb de KRsecurity et Peter Malone (selon l'avis d'Apple) |
L'avis d'Apple pour CVE-2026-43687 documente l'impact et la version du correctif. Il ne documente pas le mécanisme technique :
nfsnode est sujet à la courseaccess(2) plutôt que stat(2)Aucune analyse technique publique n'a été trouvée au moment de la rédaction. Ce dépôt comble cette lacune avec une analyse de rétro-ingénierie indépendante du kext NFS entre 26.6 et 26.7, et un PoC fonctionnel qui reproduit la course sur une cible active.
Il ne s'agit pas d'une revendication de découverte. Le CVE a été signalé par R4mbb et Peter Malone et corrigé par Apple. La contribution ici est l'analyse technique et la reproduction.
_nfs_vnop_access dans le client NFS lit nfsnode+0x158 — le pointeur vers le tableau de cache d'accès par UID — deux fois au sein d'un même appel, sans détenir aucun verrou :
; 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
L'écrivain, _nfs_nget, réalloue le tableau avec kalloc_data et stocke le résultat à +0x158 chaque fois que le client voit un nouvel UID provenant du serveur :
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
Si un autre thread atteint _nfs_nget sur le même nfsnode entre les deux chargements du lecteur, le second chargement renvoie le nouveau pointeur tandis que la première lecture — déjà utilisée pour calculer un offset — était basée sur l'ancien. Le déréférencement qui suit lit depuis le tas du noyau libéré.
Le correctif 26.7 :
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
et dans _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
Changement de disposition de la structure :
_nfs_nget et _nfs_vnop_access entre les kexts NFS 26.6 et 26.7 — voir
docs/PATCH_DIFF.mdnfsnode+0x158 en 26.6lck_rw_t inséré à +0x158, pointeur du cache
déplacé à +0x168, lck_rw_lock_shared pris autour de la lectureaccess(2) entre dans _nfs_vnop_access ; stat(2)
passe par _nfs_getattr et n'atteint jamais la fonction vulnérableVoir docs/ANALYSIS.md pour l'analyse complète et
docs/ARTIFACTS.md pour les adresses et des échantillons de journaux.
poc.sh — un PoC autonome en un seul fichier :
nfsd d'Apple pour libérer le port 2049127.0.0.1 qui fait tourner
l'UID rapporté dans chaque réponsenoacaccess(2) via test -r /
test -wnfs_vnop_access, enregistrant tout appel
où nfsnode+0x158 change en cours d'appel_nfs_vnop_access observant le tableau
changer au cours d'un même appelLa course est la condition préalable à la divulgation. Pour transformer la course en
une fuite réelle, un attaquant devrait observer le lecteur utilisant le
pointeur obsolète et propager ces octets quelque part où il peut les lire. Sur
arm64e, la valeur à nfsnode+0x158 est signée PAC et la clé par démarrage
n'est pas disponible depuis l'espace utilisateur, donc dtrace seul peut observer la course
mais ne peut pas décoder le pointeur. Voir la section « Chemins testés et écartés »
dans docs/ANALYSIS.md.
L'impact démontré est la course elle-même — la fenêtre exacte que le correctif par verrou RW de 26.7 ferme.
dtrace disponible (peut nécessiter un ajustement du SIP sur certaines installations)python3, dscl, mountLe PoC s'exécute entièrement sur la cible. Le serveur NFS malveillant se lie à
127.0.0.1 et le montage se fait via loopback. Cela garde le PoC
autonome et reproductible sans configuration réseau.
chmod +x poc.sh
sudo ./poc.sh
Réglage optionnel via des variables d'environnement :
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNT définit le nombre de threads de martèlement par utilisateur (utilise
les comptes nfsuserNNN existants, les créant au besoin).
RUN_SECONDS définit la fenêtre dtrace.
[*] 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
===============================================
Chaque ligne RACE correspond à un appel de nfs_vnop_access où le pointeur du cache d'accès a changé en cours d'appel.
Sur un système corrigé (26.7 / 27), la même charge de travail produit zéro événement RACE. Voir docs/PATCH_DIFF.md pour la
comparaison de désassemblage.
Deux bugs dans le serveur du PoC ont été corrigés pendant le développement et sont documentés ici afin que d'autres personnes construisant des outils similaires ne les rencontrent pas :
ACCESS3resok nécessite post_op_attr, pas fattr3. La RFC 1813
définit la réponse comme post_op_attr obj_attributes; uint32 access;.
post_op_attr inclut un préfixe bool avant le fattr3. Omettre
ce bool rend la réponse 4 octets trop courte ; le client la rejette silencieusement
et le montage ne devient jamais utilisable.
LOOKUP pour les noms AppleDouble (._*) doit renvoyer NFS3ERR_NOENT
(2), pas NFS3ERR_STALE (70). macOS sonde les fichiers annexes ._<name>
lors de la résolution normale de chemin. Renvoyer STALE empoisonne le montage.
Les deux sont documentés dans docs/ANALYSIS.md.
Les valeurs out dans la sortie RACE ont un motif constant sur les 48 bits de poids faible
(...7e0023297878) à travers différents nfsnodes, ne variant que dans les 16 bits de poids fort. Cela confirme que le champ est signé PAC ou obfusqué, et non un
pointeur brut du noyau. Vous pouvez observer la transition d'état (NULL →
peuplé) depuis l'espace utilisateur, mais vous ne pouvez pas décoder ni déréférencer le
pointeur sans la clé PAC par démarrage du noyau.
Pour la même raison, la symbolisation par rapport au KDK n'est pas utile pour ces valeurs — ce ne sont pas des adresses relatives au texte.
Ce dépôt est fourni à des fins de recherche en sécurité défensive et d'éducation uniquement.
MIT. Voir LICENSE.
| Offset | macOS 26.6 | macOS 26.7 |
|---|
+0x158 | pointeur du tableau de cache | lck_rw_t |
+0x160 | compteur du cache | (partie du verrou) |
+0x168 | (autre) | pointeur du tableau de cache |
+0x170 | (autre) | compteur du cache |