
Заметки по обратной разработке и самодостаточный PoC для гонки в кэше доступа клиента NFS в macOS (CVE-2026-43687), с diff дизассемблирования kext и захватом гонки через dtrace.
Независимая обратная разработка гонки в кэше доступа клиента NFS в macOS (CVE-2026-43687), а также рабочий PoC, который вызывает гонку и фиксирует её в реальном времени с помощью dtrace.
Ошибка представляет собой несинхронизированное чтение указателя кэша
доступа nfsnode в _nfs_vnop_access. Вредоносный сервер NFSv3 может
заставить кэш доступа перераспределиться в тот момент, когда другой поток
читает его, что приводит к раскрытию памяти ядра, на которое может влиять
враждебный сервер. macOS 26.7 исправляет это путём вставки lck_rw_t по
адресу nfsnode+0x158 и взятия его в разделяемом режиме вокруг чтения
кэша.
| Поле | Значение |
|---|---|
| CVE | CVE-2026-43687 |
| Компонент | com.apple.filesystems.nfs (_nfs_vnop_access) |
| Затронуто | macOS Tahoe 26.6 и более ранние, iOS 26.x и более ранние |
| Исправлено в | macOS Tahoe 26.7, macOS Golden Gate 27, iOS 26.7, iOS 27 |
| Влияние по бюллетеню | "Подключение к вредоносному серверу NFS может раскрыть память ядра." |
| CVSS v3.1 | 6.5 (Средний) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| Сообщено | R4mbb из KRsecurity и Peter Malone (согласно бюллетеню Apple) |
Бюллетень Apple по CVE-2026-43687 описывает влияние и версию с исправлением. Он не описывает технический механизм:
nfsnode подвержено гонкеaccess(2), а не stat(2)На момент написания публичного технического разбора найдено не было. Этот репозиторий восполняет этот пробел независимым анализом обратной разработки NFS kext между 26.6 и 26.7, а также рабочим PoC, который воспроизводит гонку на живой цели.
Это не заявка на открытие. CVE был сообщён R4mbb и Peter Malone и исправлен Apple. Вклад здесь — это технический анализ и воспроизведение.
_nfs_vnop_access в клиенте NFS читает nfsnode+0x158 — указатель на
массив кэша доступа для каждого UID — дважды в рамках одного вызова,
не удерживая никакой блокировки:
; 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
Писатель, _nfs_nget, перераспределяет массив с помощью kalloc_data и
сохраняет результат по +0x158 всякий раз, когда клиент видит новый UID
от сервера:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
Если другой поток достигает _nfs_nget на том же nfsnode между двумя
загрузками читателя, вторая загрузка возвращает новый указатель, тогда как
первое чтение — уже использованное для вычисления смещения — было основано
на старом. Последующее разыменование читает из освобождённой кучи ядра.
Исправление в 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
и в _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
Изменение структуры:
| Смещение | macOS 26.6 | macOS 26.7 |
|---|---|---|
+0x158 | указатель на массив кэша | lck_rw_t |
+0x160 | счётчик кэша | (часть блокировки) |
+0x168 | (другое) | указатель на массив кэша |
+0x170 | (другое) | счётчик кэша |
_nfs_nget и _nfs_vnop_access между
NFS kext 26.6 и 26.7 — см.
docs/PATCH_DIFF.mdnfsnode+0x158 в 26.6lck_rw_t вставлен по +0x158,
указатель кэша перемещён на +0x168, lck_rw_lock_shared берётся
вокруг чтенияaccess(2) входит в _nfs_vnop_access;
stat(2) идёт через _nfs_getattr и никогда не достигает уязвимой
функцииПолный разбор см. в docs/ANALYSIS.md, а адреса и
образцы логов — в docs/ARTIFACTS.md.
poc.sh — самодостаточный PoC в одном файле:
nfsd от Apple, чтобы освободить порт 2049127.0.0.1, который меняет
сообщаемый UID в каждом ответеnoacaccess(2) через test -r / test -wnfs_vnop_access, записывая любой вызов,
в котором nfsnode+0x158 меняется в середине вызова_nfs_vnop_access, наблюдающее
изменение массива в течение одного вызоваГонка — это предварительное условие для раскрытия. Чтобы превратить гонку
в реальную утечку, атакующему потребовалось бы наблюдать, как читатель
использует устаревший указатель, и распространить эти байты туда, откуда
он может их прочитать. На arm64e значение по nfsnode+0x158 подписано
PAC, а ключ, уникальный для каждой загрузки, недоступен из пользовательского
пространства, поэтому один только dtrace может наблюдать гонку, но не
может декодировать указатель. См. раздел "Paths tested and ruled out" в
docs/ANALYSIS.md.
Продемонстрированное влияние — это сама гонка — именно то окно, которое закрывает исправление с RW-блокировкой в 26.7.
dtrace (в некоторых установках может потребоваться
корректировка SIP)python3, dscl, mountPoC полностью выполняется на цели. Вредоносный сервер NFS привязывается
к 127.0.0.1, а монтирование идёт через loopback. Это делает PoC
самодостаточным и воспроизводимым без настройки сети.
chmod +x poc.sh
sudo ./poc.sh
Необязательная настройка через переменные окружения:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNT задаёт количество hammer-потоков для каждого пользователя
(используются любые существующие учётные записи nfsuserNNN, при
необходимости создаются). RUN_SECONDS задаёт окно dtrace.