
Eintägige Analyse + Sanitizer-PoC von CVE-2026-43805 (IOKit IODMACommand DriverKit RPC Race, CWE-362)
IODMACommand DriverKit-RPC-RaceEine One-Day-Analyse der Apple-IOKit-Race-Condition, die in iOS/iPadOS 26.6, macOS Tahoe 26.6, Sequoia 15.7.8, Sonoma 14.8.8 und watchOS 26.6 behoben wurde. Zum Zeitpunkt der Erstellung existierte kein öffentlicher Proof-of-Concept, daher besteht die Arbeit hier darin: den gepatchten Code durch Lesen des primären XNU-Quellcodes zu lokalisieren, die Ursache bis hinunter zum freigegebenen Objekt zu erklären und den Defekt als getreues, sanitizer-geprüftes Modell zu reproduzieren.
Apples Sicherheitshinweis ist knapp:
Eine Race-Condition wurde durch verbesserte Zustandsbehandlung behoben. Eine App kann möglicherweise eine unerwartete Systembeendigung verursachen oder Kernelspeicher schreiben.
Klassifiziert als CWE-362 (gleichzeitige Ausführung mit unsachgemäßer Synchronisation), CVSS 9.8. Sicherheitshinweise: 128066 · 128067 · 128068 · 128071 · 128072.
IODMACommand wird DriverKit-Erweiterungen (dexts) über RPC zugänglich gemacht. Die kernel-seitigen Implementierungen dieser RPC-Einstiegspunkte sind die _Impl-Methoden in
iokit/Kernel/IOUserServer.cpp.
Ein dext steuert einen DMA-Puffer über drei von ihnen:
| RPC-Methode | Zeile (xnu-12377.121.6) | Hält fInternalState->fDextLock? |
|---|---|---|
PrepareForDMA_Impl | 1009 | ja — IOLockLock @1033, Unlock @1078 |
CompleteDMA_Impl | 1084 | ja — IOLockLock @1096, Unlock @1118 |
PerformOperation_Impl | 1154 | nein — liest fActive und fMemory ohne Sperre |
Die Sperre existiert. IODMACommand::init allokiert sie
(iokit/Kernel/IODMACommand.cpp
Zeile 342). Zwei der drei Geschwister nehmen sie. Das dritte nicht — und das dritte
ist dasjenige, das den gemeinsamen Speicherdeskriptor dereferenziert. Diese Asymmetrie ist die
Schwachstelle.
xnu-12377.121.6 (macOS 26.x, das neueste Tag, für das Apple Quellcode veröffentlicht hat) zeigt
weiterhin das ungesperrte PerformOperation_Impl, es ist also der verwundbare Baum; der 26.6-
Quellcode-Drop, der den Fix enthält, ist noch nicht öffentlich. Der relevante Quellcode ist gespiegelt
in analysis/IOUserServer_excerpt.cpp.
PerformOperation_Impl führt eine Time-of-Check-Prüfung auf fActive durch und verwendet dann 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 führt den Teardown unter der Sperre aus:
IOLockLock(fInternalState->fDextLock);
...
ret = clearMemoryDescriptor(true); // see below
...
IOLockUnlock(fInternalState->fDextLock);
und clearMemoryDescriptor(true) (IODMACommand.cpp Zeile 455) ist das, was tatsächlich das freigibt, worauf PerformOperation zugreifen will:
if (fMemory) {
while (fActive) {
complete(); // drains fActive to 0
}
...
fMemory.reset(); // releases the IOMemoryDescriptor
}
Die gefährliche Verschachtelung ist also:
PerformOperation_Impl übergibt if (!fActive), während der Befehl noch aktiv ist.CompleteDMA_Impl nimmt die Sperre, leert fActive und führt fMemory.reset() aus,
wodurch der Speicherdeskriptor freigegeben wird und sein verdrahteter Backing-Speicher freigegeben bzw. wiederverwendet werden kann.PerformOperation_Impl, das noch läuft, dereferenziert das nun veraltete fMemory in
getLength() und dann in writeBytes().Da PerformOperation die Sperre nie genommen hat, kann Schritt 2 ungehindert in die Lücke
zwischen Schritt 1 und 3 fallen. writeBytes kopiert vom Aufrufer bereitgestellte Bytes in ein DMA-Mapping,
dessen Backing-Speicher freigegeben wurde – sobald diese Seite für eine andere Kernel-
Allokation wiederverwendet wird, ist dies das „Schreiben in Kernel-Speicher", vor dem der Advisory warnt. Trifft man es stattdessen auf dem Lesepfad, erhält man den Use-after-free-Lesezugriff bzw. eine unerwartete Beendigung.
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
Das Auslösen des echten Pfads erfordert ein signiertes DriverKit-Dext mit Apples
DriverKit-Entitlement auf einem verwundbaren Build, was von einem Windows-Host
aus nicht erreichbar ist. Stattdessen implementiert poc/race_model.cpp die exakte
Shared-State-Logik der drei Methoden neu — dieselbe Lock-Disziplin, dieselbe
Reihenfolge von Prüfen-dann-Freigeben-dann-Nutzen — sodass der Defekt direkt beobachtet werden kann:
PrepareForDMA und CompleteDMA nehmen den Mutex; PerformOperation nicht
(es sei denn, es wird mit -DFIXED gebaut, was die eine fehlende Sperre hinzufügt).CompleteDMA gibt den Deskriptor frei, analog zu fMemory.reset().PerformOperation in einem Thread und CompleteDMA in
einem anderen aus und platziert den Teardown innerhalb des Race-Fensters mit einem atomaren Signal
(nur Timing — es ist niemals eine Sperre auf dem Shared State).cd poc
bash run.sh
Verwundbarer Build:
operator delete innerhalb von CompleteDMA
konkurriert mit dem getLength()-Lesezugriff innerhalb von PerformOperation, wobei der Mutex nur vom
Deleter gehalten wird. Vollständiges Log: evidence/tsan_vulnerable.txt.CompleteDMA freigegebene Objekt wird in PerformOperation::getLength gelesen, allokiert in
PrepareForDMA. Vollständiges Log: 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
Der feste Build (-DFIXED, die Sperre, die zu PerformOperation hinzugefügt wurde): Beide Sanitizer sind sauber und jede Operation serialisiert korrekt gegenüber dem Teardown. Logs:
evidence/tsan_fixed.txt,
evidence/asan_fixed.txt.
Dies beweist, dass die Race-Condition in der Logik real ist und dass die Sperre sowohl notwendig als auch ausreichend ist. Es handelt sich nicht um einen Live-Kernel-Panic — siehe Umfang unten.