Search
Write a publication
Pull to refresh

Утечка хендлов в IP Helper API: как мы нашли и обошли ещё один баг Windows

Level of difficultyMedium
Reading time4 min
Views324

После статьи о баге в CancelIoEx решил рассказать ещё об одном дефекте в системных компонентах Windows — на этот раз в IP Helper (часть Windows API, отвечающая за работу с сетевой статистикой и таблицами соединений).

Этот API, среди прочего, даёт возможность сопоставлять перехваченные на уровне сети пакеты с локальными процессами. Казалось бы, проверенный механизм, который работает «под капотом» множества утилит и сетевых фильтров. Но в ходе тестирования WireSock Secure Connect в режиме split tunneling по процессам мы наткнулись на утечку, способную за считанные минуты выбить лимит хендлов в системе.

Поводом для расследования стало сообщение в нашей группе поддержки WireSock в Telegram: один из пользователей заметил аномально быстрый рост числа открытых дескрипторов процессов. Проблема стабильно воспроизводилась под нагрузкой и исчезала, если использовать фильтрацию по IP-адресам. Это стало первой зацепкой, которая в итоге вывела нас на баг в реализации IP Helper.

Отдельное спасибо пользователю @dno5iq, который обнаружил проблему, выполнил реверсинг GetOwnerModuleFromPidAndInfo и помог подтвердить наличие дефекта в её реализации.


Как мы докопались до причины

Дальнейший разбор мы провели прямо в чате, в формате мини-расследования. Достаточно быстро стало ясно, что утечки хендлов возникают только при работе в режиме split tunneling по процессам, а в режиме фильтрации по IP-адресам (AllowedIPs/DisallowedIPs) всё работает корректно. Это исключило влияние сетевой логики и позволило сосредоточиться на коде, отвечающем за определение имени процесса по PID.

В WireSock сопоставление перехваченного на уровне сети пакета с локальным процессом выполняется через таблицу сетевых соединений, которую можно получить с помощью IP Helper. Именно этот API мы используем, чтобы по PID определить имя исполняемого модуля процесса.

Напрямую хендлы на процессы в WireSock нигде не открываются. Однако функции IP Helper API — GetOwnerModuleFromTcpEntry, GetOwnerModuleFromTcp6Entry, GetOwnerModuleFromUdpEntry и GetOwnerModuleFromUdp6Entry — могут внутренне открывать хендл к процессу-владельцу для получения информации о модуле. Эти хендлы не возвращаются вызывающему коду, и Windows сама должна закрывать их внутри. Но под нагрузкой что-то явно шло не так.

Все перечисленные функции обращаются к одной и той же внутренней — GetOwnerModuleFromPidAndInfo. После её декомпиляции выяснилось, что она действительно открывает хендл к процессу, а затем выполняет две проверки:

  1. успешность получения хендла;

  2. успешность получения пути к исполняемому модулю через GetModuleFileNameExW.

Если первая проверка проходит (процесс всё ещё присутствует в системе за счёт оставшихся референсов, хотя уже завершён), а вторая — нет, функция выходит, так и не закрыв ранее открытый хендл. Это и приводит к накапливающейся утечке под нагрузкой.


Псевдокод упрощённой логики

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);

if (hProcess) {
        // Пытаемся получить путь к модулю процесса
        if (!GetModuleFileNameExW(hProcess, NULL, pathBuffer, bufferSize)) {
            // Выход без CloseHandle — утечка гарантирована
            return ERROR;
        }
     // Дальнейшая работа с именем процесса
     CloseHandle(hProcess);
}

Как мы обошли баг

Когда стало ясно, что причина утечек находится внутри IP Helper, рассчитывать на быстрое исправление со стороны Microsoft было бы чересчур оптимистично. Подобные дефекты могут жить в системе годами, а если патч и выйдет, то затронет лишь поддерживаемые версии Windows. Для части пользователей это и вовсе не актуально — некоторые до сих пор не спешат прощаться с Windows 7.

В качестве временного (а по факту — постоянного) решения я переписал резолвер процессов, чтобы не полагаться на внутреннюю реализацию GetOwnerModuleFromPidAndInfo для получения имени процесса. Теперь:

  • хендлы открываются и закрываются явно в нашем коде;

  • добавлено кеширование запросов для ускорения частых обращений;

  • итоговая производительность в нашем сценарии даже выше, чем у оригинального IP Helper.

Исходный код нового резолвера процессов доступен в репозитории ProxiFyre и применяется как в ProxiFyre, так и в WireSock Secure Connect. Внесённые изменения значительно повысили стабильность и отзывчивость работы в сценариях с высокой динамикой сетевых подключений, когда соединения постоянно создаются и закрываются. Такой режим характерен для множества приложений, но особенно заметен эффект при использовании торрент-клиентов: ускорилось установление пиров, снизилось количество разрывов соединений и улучшилась общая пропускная способность при обмене файлами. Исправление уже включено в WireSock Secure Connect начиная с версии 2.4.18, а ссылки на ранние сборки и обсуждение доступны в нашей группе поддержки в Telegram: https://t.me/wiresock.


Выводы

Даже системные API, на которые полагаются разработчики, могут содержать дефекты, проявляющиеся только в специфических условиях. Поиск таких багов требует воспроизводимых тестов под нагрузкой, изоляции подозрительных участков и, при необходимости, анализа внутренней реализации.

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

Tags:
Hubs:
+1
Comments2

Articles