
تحليل ليوم واحد + إثبات مفهوم (PoC) لمعقّم CVE-2026-43805 (حالة تسابق في IOKit IODMACommand DriverKit RPC، CWE-362)
IODMACommand DriverKitتحليل ليوم واحد لحالة السباق في Apple IOKit التي تم إصلاحها في iOS/iPadOS 26.6، وmacOS Tahoe 26.6، وSequoia 15.7.8، وSonoma 14.8.8، وwatchOS 26.6. لم يكن هناك إثبات مفهوم عام عندما كُتب هذا، لذا فإن العمل هنا هو: تحديد الكود المُرقَّع من خلال قراءة مصدر XNU الأساسي، وشرح السبب الجذري وصولاً إلى الكائن المُحرَّر، وإعادة إنتاج العيب كنموذج أمين مُتحقَّق منه بأدوات التعقيم.
إشعار Apple مقتضب:
تمت معالجة حالة سباق عبر تحسين التعامل مع الحالة. قد يتمكن تطبيق من التسبب في إنهاء غير متوقع للنظام أو الكتابة في ذاكرة النواة.
مُصنَّف CWE-362 (تنفيذ متزامن مع مزامنة غير سليمة)، CVSS 9.8. الإشعارات: 128066 · 128067 · 128068 · 128071 · 128072.
يُعرَّض IODMACommand لإضافات DriverKit (dexts) عبر RPC. التطبيقات
الخاصة بتلك النقاط الدخولية لـ RPC على جانب النواة هي طرق _Impl في
iokit/Kernel/IOUserServer.cpp.
يقود dext مخزن DMA عبر ثلاثة منها:
| طريقة RPC | السطر (xnu-12377.121.6) | هل تحتفظ بـ fInternalState->fDextLock؟ |
|---|---|---|
PrepareForDMA_Impl | 1009 | نعم — IOLockLock @1033، فتح القفل @1078 |
CompleteDMA_Impl | 1084 | نعم — IOLockLock @1096، فتح القفل @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، ثم يستخدم 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 ينفّذ عملية التفكيك تحت القفل:
IOLockLock(fInternalState->fDextLock);
...
ret = clearMemoryDescriptor(true); // see below
...
IOLockUnlock(fInternalState->fDextLock);
وclearMemoryDescriptor(true) (السطر 455 في IODMACommand.cpp) هو ما يقوم فعليًا
بتحرير الشيء الذي يكون 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، وهي لا تزال قيد التشغيل، إسنادًا مرجعيًا إلى fMemory
الذي أصبح قديمًا في getLength() ثم في writeBytes().ولأن PerformOperation لم تأخذ القفل قط، فإن الخطوة 2 حرة في أن تحلّ في الفجوة
بين الخطوتين 1 و3. تنسخ writeBytes بايتات مقدَّمة من المستدعي إلى تعيين DMA
تم تحرير دعمه — وبمجرد إعادة استخدام تلك الصفحة لتخصيص نواة آخر، فهذا هو "كتابة ذاكرة
النواة" الذي يحذّر منه التنبيه. أصبْه على مسار القراءة بدلًا من ذلك فتحصل على قراءة
استخدام-بعد-تحرير / إنهاء غير متوقع.
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
يتطلب تفعيل المسار الحقيقي وجود DriverKit dext موقّع بامتياز DriverKit من Apple على إصدار عرضة للاستغلال، وهو أمر غير قابل للوصول من مضيف Windows. بدلاً من ذلك، يعيد poc/race_model.cpp تنفيذ منطق الحالة المشتركة الدقيق للطرق الثلاث — نفس انضباط القفل، ونفس ترتيب الفحص-ثم-التحرير-ثم-الاستخدام — بحيث يمكن ملاحظة العيب مباشرةً:
PrepareForDMA وCompleteDMA القفل (mutex)؛ بينما لا تفعل PerformOperation ذلك (ما لم تُبنَ باستخدام -DFIXED، الذي يضيف القفل الناقص الوحيد).CompleteDMA الواصف (descriptor)، بما يحاكي fMemory.reset().PerformOperation على خيط واحد والدالة CompleteDMA على خيط آخر، واضعاً عملية التدمير داخل نافذة التسابق بإشارة ذرّية (atomic signal) (للتوقيت فقط — فهي ليست أبداً قفلاً على الحالة المشتركة).cd poc
bash run.sh
بناء مُعرَّض للثغرة:
operator delete داخل CompleteDMA
يتسابق مع قراءة getLength() داخل PerformOperation، مع كون القفل (mutex) محتجزًا من
قِبل المُحرِّر فقط. السجل الكامل: 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): كلا المُعقّمين نظيفان وكل عملية تتسلسل بشكل صحيح مقابل الإنهاء. السجلات:
evidence/tsan_fixed.txt،
evidence/asan_fixed.txt.
هذا يُثبت أن حالة التسابق حقيقية في المنطق وأن القفل ضروري وكافٍ معًا. وهو ليس kernel panic حيًّا — انظر النطاق أدناه.
اجعل PerformOperation_Impl يُمسك بـ fInternalState->fDextLock عبر فحص fActive وكل وصول إلى fMemory، تمامًا كما يفعل شقيقاه بالفعل. لم يُنشر مصدر Apple 26.6 بعد، لذا فإن analysis/patch.reconstructed.diff
هو الإصلاح المُعاد بناؤه من نص الاستشارة ومن تماثل الشقيقين. عندما يصدر المصدر الرسمي، فإن مقارنة IOUserServer.cpp مع xnu-12377.121.6 ستؤكد (أو تصحح) ذلك.