
CVE-2026-43805(IOKit IODMACommand DriverKit RPC 경쟁 조건, CWE-362)에 대한 하루 만의 분석 + 새니타이저 PoC
IODMACommand DriverKit RPC 경쟁 조건iOS/iPadOS 26.6, macOS Tahoe 26.6, Sequoia 15.7.8, Sonoma 14.8.8 및 watchOS 26.6에서 수정된 Apple IOKit 경쟁 조건에 대한 one-day 분석입니다. 이 글을 작성할 당시 공개된 proof-of-concept은 존재하지 않았으므로, 여기서의 작업은 다음과 같습니다: 기본 XNU 소스를 읽어 패치된 코드를 찾아내고, 해제된 객체에 이르기까지 근본 원인을 설명하며, 결함을 충실하고 sanitizer로 검증된 모델로 재현하는 것입니다.
Apple의 권고문은 간결합니다:
경쟁 조건이 개선된 상태 처리로 해결되었습니다. 앱이 예기치 않은 시스템 종료를 일으키거나 커널 메모리에 쓸 수 있습니다.
CWE-362(부적절한 동기화를 동반한 동시 실행)로 분류되며, CVSS 9.8입니다. 권고문: 128066 · 128067 · 128068 · 128071 · 128072.
IODMACommand는 RPC를 통해 DriverKit 확장(dext)에 노출됩니다. 해당 RPC 진입점의 커널 측 구현은
iokit/Kernel/IOUserServer.cpp의 _Impl 메서드입니다.
dext는 그중 세 가지를 통해 DMA 버퍼를 구동합니다:
| RPC 메서드 | 줄 (xnu-12377.121.6) | fInternalState->fDextLock을 보유하는가? |
|---|---|---|
PrepareForDMA_Impl | 1009 | 예 — IOLockLock @1033, unlock @1078 |
CompleteDMA_Impl | 1084 | 예 — IOLockLock @1096, unlock @1118 |
PerformOperation_Impl | 1154 | 아니오 — fActive와 fMemory를 잠금 없이 읽음 |
락은 존재합니다. IODMACommand::init이 이를 할당합니다
(iokit/Kernel/IODMACommand.cpp
342번째 줄). 세 형제 중 두 개는 이 락을 획득합니다. 세 번째는 획득하지 않으며 — 그리고 세 번째가 바로 공유 메모리 디스크립터를 역참조하는 쪽입니다. 이러한 비대칭이 취약점입니다.
xnu-12377.121.6(macOS 26.x, Apple이 소스를 공개한 최신 태그)은 여전히 잠금 없는 PerformOperation_Impl을 보여주므로 취약한 트리입니다. 수정 사항을 담은 26.6 소스 배포본은 아직 공개되지 않았습니다. 관련 소스는
analysis/IOUserServer_excerpt.cpp에 미러링되어 있습니다.
PerformOperation_Impl은 fActive에 대해 검사 시점(time-of-check)을 수행한 다음, 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은 락을 보유한 상태에서 해제(teardown)를 실행합니다:
IOLockLock(fInternalState->fDextLock);
...
ret = clearMemoryDescriptor(true); // see below
...
IOLockUnlock(fInternalState->fDextLock);
clearMemoryDescriptor(true)(IODMACommand.cpp 455행)는 PerformOperation이 곧 접근하려는 대상을 실제로 해제하는 부분입니다:
if (fMemory) {
while (fActive) {
complete(); // drains fActive to 0
}
...
fMemory.reset(); // releases the IOMemoryDescriptor
}
따라서 위험한 인터리빙은 다음과 같다:
PerformOperation_Impl이 명령이 여전히 활성 상태인 동안 if (!fActive)를 통과한다.CompleteDMA_Impl이 락을 획득하고, fActive를 소진한 뒤 fMemory.reset()을 수행하여
메모리 디스크립터를 해제하고, 그 와이어드 백킹이 해제/재사용되도록 한다.PerformOperation_Impl이 getLength()에서 이제는 오래된 fMemory를
역참조한 다음 writeBytes()를 호출한다.PerformOperation이 락을 전혀 획득하지 않았기 때문에, 2단계는 1단계와 3단계 사이의
틈에 끼어들 수 있다. writeBytes는 호출자가 제공한 바이트를 백킹이 이미 해제된 DMA 매핑에
복사한다 — 해당 페이지가 다른 커널 할당에 재사용되면, 이것이 바로 권고문이 경고하는
"커널 메모리 쓰기"이다. 대신 읽기 경로에서 이 상황이 발생하면 use-after-free 읽기 /
예기치 않은 종료가 발생한다.
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
실제 경로를 트리거하려면 취약한 빌드에서 Apple의 DriverKit entitlement가 적용된 서명된 DriverKit dext가 필요하며, 이는 Windows 호스트에서는 접근할 수 없습니다. 대신 poc/race_model.cpp는 세 메서드의 정확한 공유 상태 로직을 재구현합니다 — 동일한 잠금 규율, 동일한 check-then-free-then-use 순서 — 따라서 결함을 직접 관찰할 수 있습니다:
PrepareForDMA와 CompleteDMA는 뮤텍스를 획득하지만, PerformOperation은 획득하지 않습니다 (-DFIXED로 빌드하지 않는 한, 이 경우 누락된 잠금 하나가 추가됩니다).CompleteDMA는 디스크립터를 해제하며, 이는 fMemory.reset()을 그대로 반영합니다.PerformOperation을, 다른 스레드에서 CompleteDMA를 실행하여, 원자적 신호(타이밍 전용 — 공유 상태에 대한 잠금이 아님)를 통해 teardown을 경쟁 윈도우 내부에 배치합니다.cd poc
bash run.sh
취약한 빌드:
CompleteDMA 내부의 operator delete가 PerformOperation 내부의 getLength() 읽기와 경쟁하며, 뮤텍스는 삭제자만 보유합니다. 전체 로그: evidence/tsan_vulnerable.txt.CompleteDMA에서 해제된 객체가 PerformOperation::getLength에서 읽히며, PrepareForDMA에서 할당됩니다. 전체 로그: 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
고정 빌드(-DFIXED, PerformOperation에 추가된 잠금): 두 새니타이저 모두
깨끗하며 모든 작업이 teardown에 대해 올바르게 직렬화됩니다. 로그:
evidence/tsan_fixed.txt,
evidence/asan_fixed.txt.
이는 경쟁 조건이 로직상 실제로 존재하며 잠금이 필요충분 조건임을 입증합니다. 실제 커널 패닉은 아닙니다 — 아래 범위를 참조하세요.
PerformOperation_Impl이 두 형제 함수가 이미 그러하듯 fActive 검사와 모든
fMemory 접근에 걸쳐 fInternalState->fDextLock을 보유하도록 만드세요. Apple의
26.6 소스는 아직 공개되지 않았으므로, analysis/patch.reconstructed.diff
는 권고문 텍스트와 형제 함수의 대칭성으로부터 재구성한 수정입니다. 공식 소스가
공개되면 IOUserServer.cpp를 xnu-12377.121.6과 diff하여 이를 확인(또는 교정)할
수 있습니다.
README.md this writeup
analysis/IOUserServer_excerpt.cpp the three real methods (xnu-12377.121.6)
analysis/patch.reconstructed.diff the inferred one-lock fix
poc/race_model.cpp faithful shared-state model (vuln + -DFIXED)
poc/Makefile poc/run.sh TSan/ASan build + A/B runner
evidence/*.txt captured sanitizer output
xnu-12377.121.6