
Análise de um dia + PoC de sanitização da CVE-2026-43805 (corrida RPC do DriverKit IODMACommand do IOKit, CWE-362)
IODMACommand do IOKitUma análise one-day da condição de corrida do IOKit da Apple corrigida no iOS/iPadOS 26.6, macOS Tahoe 26.6, Sequoia 15.7.8, Sonoma 14.8.8 e watchOS 26.6. Não existia proof-of-concept público quando isto foi escrito, portanto o trabalho aqui é: localizar o código corrigido lendo o código-fonte primário do XNU, explicar a causa raiz até o objeto liberado, e reproduzir o defeito como um modelo fiel e verificado por sanitizer.
O aviso da Apple é sucinto:
Uma condição de corrida foi corrigida com melhor tratamento de estado. Um app pode ser capaz de causar encerramento inesperado do sistema ou escrever na memória do kernel.
Classificado como CWE-362 (execução concorrente com sincronização inadequada), CVSS 9.8. Avisos: 128066 · 128067 · 128068 · 128071 · 128072.
O IODMACommand é exposto às extensões DriverKit (dexts) via RPC. As implementações
no lado do kernel desses pontos de entrada RPC são os métodos _Impl em
iokit/Kernel/IOUserServer.cpp.
Uma dext conduz um buffer DMA através de três deles:
| Método RPC | linha (xnu-12377.121.6) | mantém fInternalState->fDextLock? |
|---|---|---|
PrepareForDMA_Impl | 1009 | sim — IOLockLock @1033, unlock @1078 |
CompleteDMA_Impl | 1084 | sim — IOLockLock @1096, unlock @1118 |
PerformOperation_Impl | 1154 | não — lê fActive e fMemory sem lock |
O lock existe. IODMACommand::init o aloca
(iokit/Kernel/IODMACommand.cpp
linha 342). Dois dos três irmãos o adquirem. O terceiro não — e o terceiro
é o que desreferencia o descritor de memória compartilhado. Essa assimetria é a
vulnerabilidade.
xnu-12377.121.6 (macOS 26.x, a tag mais recente para a qual a Apple publicou código-fonte) ainda
mostra o PerformOperation_Impl sem lock, portanto é a árvore vulnerável; o drop de código-fonte
26.6 que contém a correção ainda não é público. O código-fonte relevante está espelhado
em analysis/IOUserServer_excerpt.cpp.
PerformOperation_Impl faz um time-of-check em fActive, depois usa fMemory:
if (!fActive) { // time-of-check (no lock held)
return kIOReturnNotReady;
}
...
if (length > fMemory->getLength()) { // time-of-use (fMemory may be gone)
return kIOReturnBadArgument;
}
...
copiedDMA = writeBytes(dmaOffset, buffer.data(), length); // write into the mapping
CompleteDMA_Impl executa o teardown sob o lock:
IOLockLock(fInternalState->fDextLock);
...
ret = clearMemoryDescriptor(true); // see below
...
IOLockUnlock(fInternalState->fDextLock);
e clearMemoryDescriptor(true) (IODMACommand.cpp linha 455) é o que realmente libera a coisa que PerformOperation está prestes a acessar:
if (fMemory) {
while (fActive) {
complete(); // drains fActive to 0
}
...
fMemory.reset(); // releases the IOMemoryDescriptor
}
Portanto, o entrelaçamento perigoso é:
PerformOperation_Impl passa if (!fActive) enquanto o comando ainda está ativo.CompleteDMA_Impl adquire o lock, drena fActive e executa fMemory.reset(),
liberando o descritor de memória e permitindo que seu backing wired seja liberado/reutilizado.PerformOperation_Impl, ainda em execução, desreferencia o fMemory agora obsoleto em
getLength() e depois em writeBytes().Como PerformOperation nunca adquiriu o lock, o passo 2 pode ocorrer livremente na lacuna
entre os passos 1 e 3. writeBytes copia bytes fornecidos pelo chamador para um mapeamento DMA
cujo backing foi liberado — uma vez que essa página seja reutilizada para outra
alocação do kernel, trata-se da "escrita em memória do kernel" sobre a qual o advisory alerta. Atinge-o no
caminho de leitura e você obtém a leitura use-after-free / encerramento inesperado.
sequenceDiagram
participant W as dext thread A — PerformOperation_Impl (no lock)
participant O as dext thread B — CompleteDMA_Impl (holds fDextLock)
W->>W: if (!fActive) → active, continue
Note over W: passed the check, about to use fMemory
O->>O: IOLockLock(fDextLock)
O->>O: clearMemoryDescriptor(true): fActive=0, fMemory.reset() ← FREE
O->>O: IOLockUnlock(fDextLock)
W->>W: fMemory->getLength() / writeBytes() ← USE-AFTER-FREE
Acionar o caminho real exige um dext DriverKit assinado com o
entitlement DriverKit da Apple em uma build vulnerável, o que não é acessível a partir de um host
Windows. Em vez disso, poc/race_model.cpp reimplementa a lógica exata
de estado compartilhado dos três métodos — mesma disciplina de lock, mesma
ordem check-then-free-then-use — para que o defeito possa ser observado diretamente:
PrepareForDMA e CompleteDMA adquirem o mutex; PerformOperation não
(a menos que compilado com -DFIXED, que adiciona o lock que falta).CompleteDMA libera o descritor, espelhando fMemory.reset().PerformOperation em uma thread e CompleteDMA em
outra, posicionando o teardown dentro da janela de corrida com um sinal atômico
(apenas temporização — nunca é um lock sobre o estado compartilhado).cd poc
bash run.sh
Build vulnerável:
operator delete dentro de CompleteDMA
competindo com a leitura de getLength() dentro de PerformOperation, com o mutex mantido
apenas pelo deleter. Log completo: evidence/tsan_vulnerable.txt.CompleteDMA é lido em PerformOperation::getLength, alocado em
PrepareForDMA. Log completo: evidence/asan_vulnerable.txt.==ERROR: AddressSanitizer: heap-use-after-free ... in MemoryDescriptor::getLength()
freed by thread T0 here: ... IODMACommand::CompleteDMA() race_model.cpp:70
allocated by thread T0: ... IODMACommand::PrepareForDMA() race_model.cpp:61
A compilação corrigida (-DFIXED, o lock adicionado a PerformOperation): ambos os sanitizers estão limpos e cada operação é serializada corretamente em relação ao teardown. Logs:
evidence/tsan_fixed.txt,
evidence/asan_fixed.txt.
Isto prova que a race é real na lógica e que o lock é tanto necessário quanto suficiente. Não é um kernel panic ao vivo — ver o escopo abaixo.