
Notas de engenharia reversa e um PoC autocontido para a race do cache de acesso do cliente NFS do macOS (CVE-2026-43687), com diff de desmontagem do kext e captura da race via dtrace.
Engenharia reversa independente da corrida (race) no cache de acesso do cliente NFS do macOS (CVE-2026-43687), além de um PoC funcional que dispara a corrida e a captura ao vivo com dtrace.
O bug é uma leitura não sincronizada do ponteiro do cache de acesso do
nfsnode em _nfs_vnop_access. Um servidor NFSv3 malicioso pode forçar
o cache de acesso a realocar enquanto outra thread o está lendo,
produzindo uma divulgação de memória do kernel que um servidor hostil
pode influenciar. O macOS 26.7 corrige isso inserindo um lck_rw_t em
nfsnode+0x158 e adquirindo-o de forma compartilhada em torno da
leitura do cache.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-43687 |
| Componente | com.apple.filesystems.nfs (_nfs_vnop_access) |
| Afetado | macOS Tahoe 26.6 e anteriores, iOS 26.x e anteriores |
| Corrigido em | macOS Tahoe 26.7, macOS Golden Gate 27, iOS 26.7, iOS 27 |
| Impacto do aviso | "Conectar-se a um servidor NFS malicioso pode divulgar memória do kernel." |
| CVSS v3.1 | 6.5 (Médio) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| Reportado por | R4mbb da KRsecurity e Peter Malone (conforme o aviso da Apple) |
O aviso da Apple para o CVE-2026-43687 documenta o impacto e a versão da correção. Ele não documenta o mecanismo técnico:
nfsnode sofre a corridaaccess(2) em vez de stat(2)Nenhum writeup técnico público foi encontrado no momento da escrita. Este repositório preenche essa lacuna com uma análise de engenharia reversa independente do kext NFS entre 26.6 e 26.7, e um PoC funcional que reproduz a corrida em um alvo ao vivo.
Isto não é uma reivindicação de descoberta. O CVE foi reportado por R4mbb e Peter Malone e corrigido pela Apple. A contribuição aqui é a análise técnica e a reprodução.
_nfs_vnop_access no cliente NFS lê nfsnode+0x158 — o ponteiro para
o array de cache de acesso por UID — duas vezes dentro de uma única
chamada, sem manter nenhum lock:
; 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
O escritor, _nfs_nget, realoca o array com kalloc_data e armazena o
resultado em +0x158 sempre que o cliente vê um novo UID vindo do
servidor:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
Se outra thread alcançar _nfs_nget no mesmo nfsnode entre as duas
cargas do leitor, a segunda carga retorna o novo ponteiro enquanto a
primeira leitura — já usada para calcular um offset — baseou-se no
antigo. A desreferência subsequente lê do heap do kernel liberado.
A correção do 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
e em _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
Mudança no layout da struct:
_nfs_nget e _nfs_vnop_access entre os
kexts NFS 26.6 e 26.7 — veja
docs/PATCH_DIFF.mdnfsnode+0x158 no 26.6lck_rw_t inserido em +0x158, ponteiro
do cache movido para +0x168, lck_rw_lock_shared adquirido em torno da leituraaccess(2) entra em _nfs_vnop_access; stat(2)
passa por _nfs_getattr e nunca alcança a função vulnerávelVeja docs/ANALYSIS.md para o writeup completo e
docs/ARTIFACTS.md para endereços e amostras de log.
poc.sh — um PoC de arquivo único e autocontido:
nfsd da Apple para liberar a porta 2049127.0.0.1 que rotaciona o
UID reportado em cada respostanoacaccess(2) via test -r /
test -wnfs_vnop_access, registrando qualquer chamada
em que nfsnode+0x158 muda no meio da chamada_nfs_vnop_access observando o array
mudar durante uma única chamadaA corrida é a pré-condição para a divulgação. Para transformar a corrida
em um vazamento real, um atacante precisaria observar o leitor usando o
ponteiro obsoleto e propagar esses bytes para algum lugar que ele possa
ler. No arm64e, o valor em nfsnode+0x158 é assinado com PAC e a chave
por boot não está disponível a partir do userland, então o dtrace sozinho
pode observar a corrida mas não pode decodificar o ponteiro. Veja a seção
"Caminhos testados e descartados" em docs/ANALYSIS.md.
O impacto demonstrado é a própria corrida — exatamente a janela que a correção de RW-lock do 26.7 fecha.
dtrace disponível (pode exigir ajuste de SIP em algumas instalações)python3, dscl, mountO PoC roda inteiramente no alvo. O servidor NFS malicioso se vincula a
127.0.0.1 e o mount é via loopback. Isso mantém o PoC autocontido e
reproduzível sem configuração de rede.
chmod +x poc.sh
sudo ./poc.sh
Ajuste opcional via variáveis de ambiente:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNT define o número de threads hammer por usuário (usa
quaisquer contas nfsuserNNN existentes, criando-as conforme
necessário). RUN_SECONDS define a janela do 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
===============================================
Cada linha RACE é uma chamada de nfs_vnop_access em que o ponteiro
do cache de acesso mudou no meio da chamada.
Em um sistema corrigido (26.7 / 27), a mesma carga de trabalho produz
zero eventos RACE. Veja docs/PATCH_DIFF.md para
a comparação de disassembly.
Dois bugs no servidor do PoC foram corrigidos durante o desenvolvimento e estão documentados aqui para que outros construindo ferramentas semelhantes não os encontrem:
ACCESS3resok requer post_op_attr, não fattr3. A RFC 1813
define a resposta como post_op_attr obj_attributes; uint32 access;.
post_op_attr inclui um prefixo bool antes do fattr3. Omitir
esse bool faz a resposta ficar 4 bytes menor; o cliente a rejeita
silenciosamente e o mount nunca se torna utilizável.
LOOKUP para nomes AppleDouble (._*) deve retornar NFS3ERR_NOENT
(2), não NFS3ERR_STALE (70). O macOS sonda por sidecars ._<name>
durante a resolução normal de caminho. Retornar STALE envenena o mount.
Ambos estão documentados em docs/ANALYSIS.md.
Os valores out na saída do RACE têm um padrão constante nos 48 bits
inferiores (...7e0023297878) entre diferentes nfsnodes, variando
apenas nos 16 bits superiores. Isso confirma que o campo é assinado
com PAC ou ofuscado, não um ponteiro de kernel bruto. Você pode
observar a transição de estado (NULL → populado) a partir do userland,
mas não pode decodificar ou desreferenciar o ponteiro sem a chave PAC
por boot do kernel.
Pela mesma razão, a simbolização contra o KDK não é útil para esses valores — eles não são endereços relativos a texto.
Este repositório é fornecido para pesquisa e educação em segurança defensiva apenas.
MIT. Veja LICENSE.
| Offset | macOS 26.6 | macOS 26.7 |
|---|
+0x158 | ponteiro do array de cache | lck_rw_t |
+0x160 | contagem do cache | (parte do lock) |
+0x168 | (outro) | ponteiro do array de cache |
+0x170 | (outro) | contagem do cache |