Утро в тот день началось с того, что у нас «сломались 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 везде одинаковая, обновления, билд нашей программы — всё идентично. Вдруг кто-то замечает, что все ноутбуки, на которых проблема воспроизвелась — одной модели. Аппаратная несовместимость? Но почему вдруг сейчас, раньше же работало… Кроме того, в офисе есть и другие ноутбуки той же модели, которые работают и сейчас. Сравнивали даже версии драйверов устройств — вроде всё одинаково. Но вот на одних ноутбуках всё работает, а на других нет.

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

  1. PIDы процессов, которые обычно являются трёх-, четырёх- или пятизначными цифрами на моём компьютере почему-то стали шестизначными. Было достаточно странно видеть PID типа 780936. Не замечал таких раньше. При этом общее количество запущенных процессов было вполне адекватным (до сотни).
  2. Диспетчер задач на вкладке 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, которое может быть весьма полезным, но требует внимательного тестирования и готовности к нестандартным проблемам.

Комментарии (3)


  1. kITerE
    21.12.2018 16:27
    +6

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

    Мне кажется, что суть проблемы не в каком-то ограниченном внутреннем буфере. Если внимательно посмотреть на SYSTEM_HANDLE_TABLE_ENTRY_INFO, то можно обнаружить, что HandleValue имеет тип USHORT, который имеет размер всего 2-а байта (поле не может быть больше 0xFFFF).


    А в новой структуре SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX это поле (HandleValue) уже имеет тип ULONG_PTR.


    1. tangro Автор
      21.12.2018 19:17

      Да, тоже может быть.


    1. mark_ablov
      22.12.2018 08:29
      +1

      ExSnapShotHandleTables и ExSnapShotHandleTablesEx идентичны за исключением используемой структуры (SYSTEM_HANDLE_TABLE_ENTRY_INFO vs SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX)