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

в 1:04, , рубрики: баги, Драйвер, разработка под windows, реверс-инжиниринг, системное программирование
IopReadyDeviceObjects: медвежья услуга от ядра и как с ней сосуществовать - 1

Предисловие

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

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

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

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

Код 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 ). Документация от WdfIoQueueCreate такого статуса возврата не упоминает - что-то пошло не так.

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

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

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

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

Ок, хардкор так хардкор. Добавляем к work item-ам автоматическую синхронизацию фреймворком (установкой AutomaticSerialization структуры WDF_WORKITEM_CONFIG в TRUE, подробнее тут) и начинаем смотреть логи. Из них я почерпнул следующее: проблема с подключением очереди возникает только с первым устройством, и то с какой-то вероятностью, и исполнение этого кода начинается либо параллельно с моей DriverEntry, либо сразу после. С помощью расстановки циклов ожидания я создал последовательность, при которой DriverEntry завершается между созданием устройства и созданием очереди к нему в work item-е - и я смог поймать проблему отладчиком. Отлично, я знаю что кто-то сбрасывает поле Flags DEVICE_OBJECT-а - давайте поставим на него breakpoint по доступу на запись (windbg: ba w4 <DEVICE_OBJECT_ADDRESS + FLAGS_OFFSET>) и поймаем нашего плохиша. Смещение поля спросим у отладчика (dt nt!_DEVICE_OBJECT Flags) - в моём случае это 0x30 (x64). Отпускаем отладчик и ловим срабатывание тут:

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

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

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

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

Что делать?

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

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

Анализ отмеченной функции IopCallDriverReinitializationRoutines дал мне возможность реализовать оба выхода из ситуации - эта функция вызывает так называемые DRIVER_REINITIALIZE callback-и. Таким образом чуть изменив порядок и зарегистрировав установку AddDevice драйвера-клиента из callback функции, установленной с помощью IoRegisterDriverReinitialization, - мы решаем исходную проблему! Забавная вещь: исходники из WRK мало отличаются от современного кода, не смотря на их древность (современный код чуть отрефакторен и содержит генерацию ETW событий на каждый чих - здорово для анализа поведения софта без хуков и прочей чернухи). После внесения этого изменения процесс будет выглядеть так:

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

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

Вывод

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

Заключение

Хоть я в разработке и достаточно давно, но драйверами плотно стал заниматься лишь недавно (стартовав и координируя 2 проекта) - и как следствие почти каждый рабочий день открываю для себя что-то новое (например наличие reader-writer spin lock-а, который позволяет получить почти EResource/PushLock на DISPATCH_LEVEL - рекомендую прочесть статью, если ещё не читали). И знаете что? Я кайфую от этого. Недавно читал забавную статью на хабре, которая упоминала три зоны - зону комфорта, зону обучения и зону паники. Так вот: похоже мне нравится находиться на границе зоны обучения и зоны паники :-) Может быть статья и чуть сумбурная, но найди я нечто подобное несколько дней назад - сэкономил бы уйму времени и не получил бы всего кайфа от анализа :-) MS вроде как и упоминает о многих вещах что я нашёл, но информация фрагментирована и по сути моей проблемы не гуглилась - надеюсь сейчас всё будет несколько иначе.

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

Автор:
Deamhan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js