Утро в тот день началось с того, что у нас «сломались if'ы». Это выражение было когда-то придумано одним моим коллегой, который демонстрировал, как у него отладчик при пошаговом проходе по коду заходит в блок if, при том, что условие, которое if проверял, было абсолютно точно равно false. Проблема в тот раз оказалась тривиальной — он использовал релизный оптимизированный билд, а при таком сценарии доверять пошаговой отладке, конечно, нельзя. Но само выражение «сломались if'ы» прижилось и использовалось у нас с тех пор для обозначения ситуации, когда перестало работать что-то настолько фундаментальное, что в это даже с трудом верилось.
Так вот, в тот день у нас сломалась функция NtQuerySystemInformation — одна из важнейших функций ОС Windows, возвращающая информацию о процессах, потоках, системных дескрипторах и т.д. О пользе от использования данной функции я когда-то писал вот эту статью. Но оказалось, что иногда могут отказывать даже подобные краеугольные камни системы.
Итак, что же произошло.
Достаточно продолжительное время (уже несколько лет) мы использовали вызов функции NtQuerySystemInformation с аргументом SystemHandleInformation для получения информации обо всех дескрипторах в системе. Да, этот аргумент формально относится к недокументированным, но если вы начнёте искать информацию о том, как перечислить все дескрипторы во всех запущенных сейчас приложениях на ОС Windows, то комбинация 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)) {
printf("NtQuerySystemInformation failed!n");
return 1;
}
for (i = 0; i < handleInfo->HandleCount; i++) {
...
}
Но вот я запускаю наше приложения — и вдруг оказывается, что нужный мне дескриптор (а я точно знаю, что он существует!) в списке возвращённых функцией NtQuerySystemInformation() отсутствует. Всё, приехали — «сломались if'ы».
Пытаемся воспроизвести проблему на других компьютерах в офисе. На некоторых воспроизводится, на большинстве — нет. Пытаемся понять, чем те, на которых воспроизводится, отличаются от тех, на которых всё хорошо. Версия Windows везде одинаковая, обновления, билд нашей программы — всё идентично. Вдруг кто-то замечает, что все ноутбуки, на которых проблема воспроизвелась — одной модели. Аппаратная несовместимость? Но почему вдруг сейчас, раньше же работало… Кроме того, в офисе есть и другие ноутбуки той же модели, которые работают и сейчас. Сравнивали даже версии драйверов устройств — вроде всё одинаково. Но вот на одних ноутбуках всё работает, а на других нет.
Вырывание волос на голове продолжался примерно полдня, пока я случайно не обратил внимание на две вещи:
- PIDы процессов, которые обычно являются трёх-, четырёх- или пятизначными цифрами на моём компьютере почему-то стали шестизначными. Было достаточно странно видеть PID типа 780936. Не замечал таких раньше. При этом общее количество запущенных процессов было вполне адекватным (до сотни).
- Диспетчер задач на вкладке CPU показывал общее количество дескрипторов в системе — и оно было огромным, более 800 000.
Для обычного приложения является нормой открыть сотню-другую дескрипторов. Ну тысячу. Хром при активном использовании может открывать около 2000, Visual Studio на больших проектах может открыть 3000. Но кто же открыл 800 000? К счастью, упомянутый ранее Process Hacker позволяет показать количество дескрипторов для каждого процесса и даже отсортировать список процессов по количеству используемых дескрипторов.
И что же мы видим? А видим мы примерно вот такую картину:
Надо сказать, что вышеуказанный скриншот я делал вот только что, поэтому у первого в списке процесса там «всего» около 20 000 дескрипторов. А тогда, когда я увидел проблему впервые, их там было около 650 000. И кто же наш герой? Бинго! Это процесс SynTPEnhService.exe.
И тут у меня в голове складывается весь пазл. SynTPEnhService.exe — это часть драйвера тачпада Synaptics. Он был установлен только на ноутбуках определённой модели у нас в офисе, на которых и случалась проблема. Короткое наблюдение показало, что каждые 5 секунд этот процесс запускает дочерний процесс SynTPEnh.exe, которые спустя 1-2 секунды закрывается. При этом родительский процесс продолжает держать дескриптор дочернего процесса, что приводит к утечке дескрипторов. По одному каждые 5 секунд. Это 17 280 дескрипторов в сутки. Оставьте компьютер включенным на недельку и вот у вас уже больше сотни тысяч зависших дескрипторов. Мой лично компьютер не перезагружался больше месяца — отсюда и PIDы новых процессов с номерами выше полумиллиона. Это же объясняет и то, почему проблема воспроизводилась на некоторых ноутбуках в нашем офисе, но не возникала на других таких же: кое-кто из моих коллег перезагружал свои ПК каждый день, а кто-то, как и я, оставлял их включенными на ночь.
Кстати в этом месте я вспомнил, что уже читал о какой-то проблеме с драйверами тачпадов Synaptics. Немного покопавшись, я нашел вот эту статью, которую написал Bruce Dawson (множество переводов его статей в разные времена публиковались и на Хабре, но не эта конкретная). Там он описывает проблему утечки памяти из-за этого бесконечного перезапуска процесса SynTPEnh.exe, но ничего не говорит о проблеме утечки дескрипторов, так что моя находка всё же отличается от его.
Решение проблемы
Итак, драйвер тачпада «съедает» сотни тысяч дескрипторов — и что с того? А то, что написанная ещё во времена Windows NT функция NtQuerySystemInformation(SystemHandleInformation,...) имела (и имеет) некоторый вполне ограниченный внутренний буфер. Я не нашел нигде точного указания его размера, но, очевидно, что он не был рассчитан на миллион дескрипторов. В итоге функция возвращает их «сколько может», а значит среди них может оказаться, а может и не оказаться искомый.
Что же делать? Как говорил Рик из мультсериала «Рик и Морти»: «Когда ты изобретаешь телепортацию, то сразу обнаруживаешь неприятную вещь: ты последний во Вселенной, кто её изобрёл». Как оказалось, Microsoft осознала эту проблему с ограниченностью буфера в NtQuerySystemInformation при вызове её с аргументом SystemHandleInformation уже лет 20 назад и поэтому, начиная с WindowsXP, они добавили функции NtQuerySystemInformation ещё один (и тоже недокументированный) аргумент SystemExtendedHandleInformation. При вызове NtQuerySystemInformation(SystemExtendedHandleInformation, ...) вам будут возвращены все дескрипторы в системе, сколько бы их ни было. Ну, вернее, я не знаю этого точно, может быть какие-то ограничения есть и для этого аргумента, но то, что 800 000 дескрипторов он вернуть в состоянии — это точно.
В сети можно найти примеры использования SystemExtendedHandleInformation, например, вот этот. В общем, там всё аналогично, просто используются другие структуры, да и на этом всё.
Это была поучительная история об использовании недокументированных аргументов ОС Widnows, которое может быть весьма полезным, но требует внимательного тестирования и готовности к нестандартным проблемам.
Автор: tangro