Команда, отвечающая за shell32.dll, получила баг-репорт: сторонняя программа сыпалась с крашами, и большая часть падений приходилась на их DLL. Они открыли дампы и увидели явные признаки переполнения стека — бесконечный цикл из вызовов ntdll!RtlLookupFunctionEntry и ntdll!KiUserExceptionDispatch. Это была спираль смерти обработки исключений: ядро не смогло обработать ошибку и вернуло её в пользовательский режим. Пока система искала нужный обработчик, возникало новое исключение — и всё начиналось заново. В итоге стек переполнился, процесс упал.
Сначала вину повесили на shell32, но если пройти по стеку до самого дна, видно, что первое исключение случилось не в нём. Исходная точка — вызов combase!CoTaskMemFree. Команда проверила регистры и нашла запись об исключении: код STATUS_ACCESS_VIOLATION, попытка выполнить код по неисполняемому адресу. И этот адрес указывал на combase!CoTaskMemFree. Сюрприз: память, где лежала combase.dll, оказалась freed — статус PAGE_NOACCESS. Вся DLL размером 3.5 MB была выгружена из памяти.
При этом загрузчик Windows думал иначе: он всё ещё числил combase.dll как загруженную (LoadCount = 0xFFFFFFFF, «закреплена»). Это значит, что библиотеку выгрузили не через FreeLibrary, а кто-то вызвал VirtualFree на её память напрямую. Вероятнее всего, сработал баг с повреждением памяти: какая-то переменная, хранящая адрес для очистки, была перезаписана адресом combase.dll, или сработала неинициализированная переменная, в которой случайно остался этот адрес. В любом случае, shell32 — не виновник, а очередная жертва: он просто первым вызвал функцию из уже удалённой DLL.
Чтобы подтвердить эту теорию, команда собрала статистику по 100 последним крашам той же программы. Краши с shell32 составляли всего 11%. Но нашлось ещё 13 других переполнений стека и куча access violation в «unknown» — всего 46% всех падений имели одну и ту же природу: во время отправки уведомления DLL_PROCESS_DETACH одна из DLL оказывалась принудительно выгружена, и крах записывался на следующую DLL, которая пыталась в неё обратиться. Это классический bucket spray — один корень порождает кучу разных типов ошибок. Хорошая новость: shell32 не виноват. Плохая: настоящий злоумышленник пока не найден.