После статьи о баге в 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
. После её декомпиляции выяснилось, что она действительно открывает хендл к процессу, а затем выполняет две проверки:
успешность получения хендла;
успешность получения пути к исполняемому модулю через 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, на которые полагаются разработчики, могут содержать дефекты, проявляющиеся только в специфических условиях. Поиск таких багов требует воспроизводимых тестов под нагрузкой, изоляции подозрительных участков и, при необходимости, анализа внутренней реализации.
В этом случае баг удалось обойти, полностью заменив проблемный вызов своей реализацией. Это не только устранило утечку, но и сделало работу приложения быстрее и надёжнее.