Хабрахабр

Проблемы использования функции NtQuerySystemInformation с недокументированными аргументами

Утро в тот день началось с того, что у нас «сломались if'ы». Это выражение было когда-то придумано одним моим коллегой, который демонстрировал, как у него отладчик при пошаговом проходе по коду заходит в блок if, при том, что условие, которое if проверял, было абсолютно точно равно false. Проблема в тот раз оказалась тривиальной — он использовал релизный оптимизированный билд, а при таком сценарии доверять пошаговой отладке, конечно, нельзя. Но само выражение «сломались if'ы» прижилось и использовалось у нас с тех пор для обозначения ситуации, когда перестало работать что-то настолько фундаментальное, что в это даже с трудом верилось.

О пользе от использования данной функции я когда-то писал вот эту статью. Так вот, в тот день у нас сломалась функция NtQuerySystemInformation — одна из важнейших функций ОС Windows, возвращающая информацию о процессах, потоках, системных дескрипторах и т.д. Но оказалось, что иногда могут отказывать даже подобные краеугольные камни системы.

Да, этот аргумент формально относится к недокументированным, но если вы начнёте искать информацию о том, как перечислить все дескрипторы во всех запущенных сейчас приложениях на ОС Windows, то комбинация NtQuerySystemInformation + SystemHandleInformation будет наиболее часто предлагаемым вариантом. Итак, что же произошло.
Достаточно продолжительное время (уже несколько лет) мы использовали вызов функции NtQuerySystemInformation с аргументом SystemHandleInformation для получения информации обо всех дескрипторах в системе. И он действительно работает, на всех ОС начиная ещё с Windows NT.

Ну, по разным причинам. Зачем может понадобиться искать дескрипторы во всех процессах? Есть программы, которые делают это ради поиска заблокированного кем-то в данный момент ресурса (например, файла). Утилиты типа Process Hacker просто показывают их в информационных целях. Или перечислить дескрипторы ради их дублирования с целью организации песочницы. А ещё можно, например, найти в чужом процессе мьютекс, использующийся для разрешения запуска лишь одной копии программы, закрыть его и позволить запустить два экземпляра такого приложения. В общем, задач много.

Код перечисления дескрипторов я здесь полностью приводить не буду, скажу лишь, что он был, в общем, аналогичен общераспространённым примерам, вроде вот этого:

while ((status = NtQuerySystemInformation( SystemHandleInformation, handleInfo, handleInfoSize, NULL )) == STATUS_INFO_LENGTH_MISMATCH) handleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH. if (!NT_SUCCESS(status)) for (i = 0; i < handleInfo->HandleCount; i++) { ... }

Но вот я запускаю наше приложения — и вдруг оказывается, что нужный мне дескриптор (а я точно знаю, что он существует!) в списке возвращённых функцией NtQuerySystemInformation() отсутствует. Всё, приехали — «сломались if'ы».

На некоторых воспроизводится, на большинстве — нет. Пытаемся воспроизвести проблему на других компьютерах в офисе. Версия Windows везде одинаковая, обновления, билд нашей программы — всё идентично. Пытаемся понять, чем те, на которых воспроизводится, отличаются от тех, на которых всё хорошо. Аппаратная несовместимость? Вдруг кто-то замечает, что все ноутбуки, на которых проблема воспроизвелась — одной модели. Сравнивали даже версии драйверов устройств — вроде всё одинаково. Но почему вдруг сейчас, раньше же работало… Кроме того, в офисе есть и другие ноутбуки той же модели, которые работают и сейчас. Но вот на одних ноутбуках всё работает, а на других нет.

Вырывание волос на голове продолжался примерно полдня, пока я случайно не обратил внимание на две вещи:

  1. PIDы процессов, которые обычно являются трёх-, четырёх- или пятизначными цифрами на моём компьютере почему-то стали шестизначными. Было достаточно странно видеть PID типа 780936. Не замечал таких раньше. При этом общее количество запущенных процессов было вполне адекватным (до сотни).
  2. Диспетчер задач на вкладке CPU показывал общее количество дескрипторов в системе — и оно было огромным, более 800 000.

Для обычного приложения является нормой открыть сотню-другую дескрипторов. Ну тысячу. Хром при активном использовании может открывать около 2000, Visual Studio на больших проектах может открыть 3000. Но кто же открыл 800 000? К счастью, упомянутый ранее Process Hacker позволяет показать количество дескрипторов для каждого процесса и даже отсортировать список процессов по количеству используемых дескрипторов.

А видим мы примерно вот такую картину: И что же мы видим?

А тогда, когда я увидел проблему впервые, их там было около 650 000. Надо сказать, что вышеуказанный скриншот я делал вот только что, поэтому у первого в списке процесса там «всего» около 20 000 дескрипторов. Бинго! И кто же наш герой? Это процесс SynTPEnhService.exe.

SynTPEnhService.exe — это часть драйвера тачпада Synaptics. И тут у меня в голове складывается весь пазл. Короткое наблюдение показало, что каждые 5 секунд этот процесс запускает дочерний процесс SynTPEnh.exe, которые спустя 1-2 секунды закрывается. Он был установлен только на ноутбуках определённой модели у нас в офисе, на которых и случалась проблема. По одному каждые 5 секунд. При этом родительский процесс продолжает держать дескриптор дочернего процесса, что приводит к утечке дескрипторов. Оставьте компьютер включенным на недельку и вот у вас уже больше сотни тысяч зависших дескрипторов. Это 17 280 дескрипторов в сутки. Это же объясняет и то, почему проблема воспроизводилась на некоторых ноутбуках в нашем офисе, но не возникала на других таких же: кое-кто из моих коллег перезагружал свои ПК каждый день, а кто-то, как и я, оставлял их включенными на ночь. Мой лично компьютер не перезагружался больше месяца — отсюда и PIDы новых процессов с номерами выше полумиллиона.

Немного покопавшись, я нашел вот эту статью, которую написал Bruce Dawson (множество переводов его статей в разные времена публиковались и на Хабре, но не эта конкретная). Кстати в этом месте я вспомнил, что уже читал о какой-то проблеме с драйверами тачпадов Synaptics. Там он описывает проблему утечки памяти из-за этого бесконечного перезапуска процесса SynTPEnh.exe, но ничего не говорит о проблеме утечки дескрипторов, так что моя находка всё же отличается от его.

Решение проблемы

Итак, драйвер тачпада «съедает» сотни тысяч дескрипторов — и что с того? А то, что написанная ещё во времена Windows NT функция NtQuerySystemInformation(SystemHandleInformation,...) имела (и имеет) некоторый вполне ограниченный внутренний буфер. Я не нашел нигде точного указания его размера, но, очевидно, что он не был рассчитан на миллион дескрипторов. В итоге функция возвращает их «сколько может», а значит среди них может оказаться, а может и не оказаться искомый.

Как говорил Рик из мультсериала «Рик и Морти»: «Когда ты изобретаешь телепортацию, то сразу обнаруживаешь неприятную вещь: ты последний во Вселенной, кто её изобрёл». Что же делать? При вызове NtQuerySystemInformation(SystemExtendedHandleInformation, ...) вам будут возвращены все дескрипторы в системе, сколько бы их ни было. Как оказалось, Microsoft осознала эту проблему с ограниченностью буфера в NtQuerySystemInformation при вызове её с аргументом SystemHandleInformation уже лет 20 назад и поэтому, начиная с WindowsXP, они добавили функции NtQuerySystemInformation ещё один (и тоже недокументированный) аргумент SystemExtendedHandleInformation. Ну, вернее, я не знаю этого точно, может быть какие-то ограничения есть и для этого аргумента, но то, что 800 000 дескрипторов он вернуть в состоянии — это точно.

В общем, там всё аналогично, просто используются другие структуры, да и на этом всё. В сети можно найти примеры использования SystemExtendedHandleInformation, например, вот этот.

Это была поучительная история об использовании недокументированных аргументов ОС Widnows, которое может быть весьма полезным, но требует внимательного тестирования и готовности к нестандартным проблемам.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть