IopReadyDeviceObjects: медвежья услуга от ядра и как с ней сосуществовать

c01be568370099927ee1e2adfd717046.jpg

Предисловие

Всем привет! Столкнулся я недавно с одной интересной и не вполне понятной с первого взгляда проблемой в KMDF драйвере, разработкой которого я в данный момент занимаюсь. Опыта в этой сфере у меня не много — это первый проект на KMDF которым я занимаюсь. В деталях описывать проект не могу (всё-таки частная собственность), да это и не нужно, но идея такова: есть 2 драйвера, один из них становится в стек устройств определённого класса и предоставляет интерфейс через который второй драйвер может подписаться на добавление новых и уже подключенных устройств (несколько callback-ов), получать обратные вызовы на определённых операциях и так далее. Таким образом первый драйвер находится в системе постоянно и для своей замены требует перезагрузки и содержит минимальную логику, а второй может свободно обновляться на ходу (без перезагрузки) и принимает решения. Логика этого драйвера подразумевает создание control device для каждого устройства-фильтра, установленного в стек (нужен дополнительный функционал без коллизий с функционалом стека) — и вот тут у меня возникла проблема, на определение причин которой и дальнейшее решение я потратил довольно много времени. Статью об этом решил написать именно сегодня — как-никак это неплохой способ сделать что-то полезное на свой профессиональный юбилей — 10 лет в разработке:-)

Суть проблемы

Система работает следующим образом: драйвер, устанавливающий фильтр в стеки (назовём его фильтр-драйвером) устройств принимает internal ioctl, содержащий адрес на callback функцию AddDevice второго драйвера (назовём его клиент-драйвером) и вызывает её сразу для уже установленных устройств и при добавлении новых при вызове собственного AddDevice PnP менеджером. С учётом особенностей реализации AddDevice клиент-драйвера (вызывается на IRQL DISPATCH_LEVEL для уже подключенных устройств) делегирует часть работы на work item для выполнения работы, требующей IRQL PASSIVE_LEVEL (это классика, тут останавливаться не будем). Чтобы это стало часть понятнее добавим чуть графики:

369943f01c73b89bffde4f1930edfac4.png


Код work item-а примерно следующий:

NTSTATUS status = WdfDeviceCreate(pInit, pDeviceAttributes, pControlDevice);
if (!NT_SUCCESS(status))
{
  // логгирование и т.п.
  WdfDeviceInitFree(pInit);
  return status;
}

// код настройки дефолтной очереди
status = WdfIoQueueCreate(*pControlDevice, pConfig, pQueueAttributes, pQueue);
if (!NT_SUCCESS(status))
{
  // логгирование и т.п.
  WdfObjectDelete(*pControlDevice); // устройство без очереди для нас бесполезно
  return status;
}

// создание дополнительной очереди для отложенной обработки IRP
WdfControlFinishInitializing(*pControlDevice);

Вроде ничего особенного, верно? Я тоже так думал, но с какой-то вероятностью создание очереди завершалось неудачей со статусом 0xC0000184 (STATUS_INVALID_DEVICE_STATE). Документация от функции (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfio/nf-wdfio-wdfioqueuecreate) такого статуса возврата не упоминает — что-то пошло не так.

Исследование проблемы

Беглый поиск не дал мне ответа — что ж, не беда. Берём IDA и смотрим что у нас в WdfIoQueueCreate:

df59231b9f9335aee6f19dc4daa8767c.png

Супер, кто-то сбросил флаг инициализации устройства и из-за этого я не могу присоединить к нему очередь. Но кто это сделал? WdfControlFinishInitializing, который этот флаг сбрасывает, вызывается позднее. Отдельный прикол в том, что с помощью отладчика подловить проблему я не смог — изменение времени операций определённо влияло на возникновение проблемы, стало быть мы имеем дело с какой-то проблемой синхронизации. В процессе дальнейшего анализа работы Wdf1000.sys и поиска найденных имён я нашёл то, чего совсем не ожидал — код WDF (KMDF & UMDF) официально доступен на гитхабе MS (https://github.com/microsoft/Windows-Driver-Frameworks). Изучение его кода помогло мне понять детали работы фреймворка, но ответа на свой вопрос я не нашёл.

Ок, хардкор так хардкор. Добавляем к work item-ам автоматическую синхронизацию фреймворком (установкой AutomaticSerialization структуры WDF_WORKITEM_CONFIG в TRUE, подробнее тут: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfworkitem/ns-wdfworkitem-_wdf_workitem_config) и начинаем смотреть логи. Из них я почерпнул следующее: проблема с подключением очереди возникает только с первым устройством, и то с какой-то вероятностью, и исполнение этого кода начинается либо параллельно с моей DriverEntry, либо сразу после. С помощью расстановки циклов ожидания я создал последовательность, при которой DriverEntry завершается между созданием устройства и созданием очереди к нему в work item-е — и я смог поймать проблему отладчиком. Отлично, я знаю что кто-то сбрасывает поле Flags DEVICE_OBJECT-а — давайте поставим на него breakpoint по доступу на запись (windbg: ba w4 ) и поймаем нашего плохиша. Смещение поля спросим у отладчика (dt nt!_DEVICE_OBJECT Flags) — в моём случае это 0×30 (x64). Отпускаем отладчик и ловим срабатывание тут:

bcba9b2a4de387eeb5c4abe93f0dadc0.png

Давайте взглянем, что это за функция такая:

7f6ad806fef3bf193ffb132b5c23eaf5.png

Проверить смещения полей можно с помощью dt nt!_DEVICE_OBJECT NextDevice и dt nt!_DRIVER_OBJECT DeviceObject . Функция тут простая как грабли и по сути просто выставляет флаги на объект драйвера и весть список его устройств. И тут кроется ответ на мой вопрос «кто инициализировал устройство из соседнего потока?» — виновник найден. По сути эта функция приведёт все мои устройства после DriverEntry в состояние готовности, хочу я этого или нет.

Что делать?

Нам нужно что-то из двух: либо перенести регистрацию AddDevice драйвера-клиента после IopReadyDeviceObjects либо как-то синхронизировать AddDevice c чем-то после функции-плохиша, но в процессе инициализации. Давайте посмотрим код функций в стеке до IopReadyDeviceObjects. IopLoadDriver не содержит чего-то, что можно было бы использовать, а вот IopLoadUnloadDriver — напротив:

a81dd1866db56161cebf2737e32887f3.png

Анализ отмеченной функции IopCallDriverReinitializationRoutines дал мне возможность реализовать оба выхода из ситуации — эта функция вызывает так называемые DRIVER_REINITIALIZE callback-и (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nc-ntddk-driver_reinitialize). Таким образом чуть изменив порядок и зарегистрировав установку AddDevice драйвера-клиента из callback функции, установленной с помощью IoRegisterDriverReinitialization, — мы решаем исходную проблему! Забавная вещь: исходники из WRK мало отличаются от современного кода, не смотря на их древность (современный код чуть отрефакторен и содержит генерацию ETW событий на каждый чих — здорово для анализа без хуков и прочей чернухи). После внесения этого изменения процесс будет выглядеть так:

77098a247d9151e72ae71cf40f121318.png

Как и ожидалось, испытание подтвердили тот факт, что проблема канула в лету :-)

Вывод

Как минимум не создавать устройств параллельно с выполнением DriverEntry и окрестного кода, если что-то подобно может иметь место — перемещаем это в DRIVER_REINITIALIZE callback. Да и наверное в целом лучше отказаться от кода, который может работать параллельно с DriverEntry. Звучит очевидно, но что имеем — то имеем. Наверное этот же совет можно применить ко множеству ситуаций.

Заключение

Хоть я в разработке и достаточно давно, но драйверами плотно стал заниматься лишь недавно (стартовав и координируя 2 проекта) — и как следствие почти каждый рабочий день открываю для себя что-то новое (например наличие read-write spin lock-а, который позволяет получить почти EResource на DISPATCH_LEVEL — рекомендую прочесть статью https://www.osr.com/nt-insider/2015-issue3/the-state-of-synchronization/ если ещё не читали). И знаете что? Я кайфую от этого. Недавно читал забавную статью на хабре, которая упоминала три зоны — зону комфорта, зону обучения и зону паники. Так вот: похоже мне нравится находиться на границе зоны обучения и зоны паники :-) Может быть статья и чуть сумбурная, но найди я нечто подобное несколько дней назад — сэкономил бы уйму времени и не получил бы всего кайфа от анализа :-) MS вроде как и упоминает о многих вещах что я нашёл, но информация фрагментирована и по сути моей проблемы не гуглилась — надеюсь сейчас всё будет несколько иначе.

Искренне благодарю за внимание.

© Habrahabr.ru