• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Поиск утечек Windows HANDLE в Chromium и других

rrv321

(L3) cache
Пользователь
Регистрация
16.11.2020
Сообщения
170
Реакции
251
Три года назад я обнаружил утечку 32 ГБ памяти, вызванную тем, что CcmExec.exe не смог закрыть хендлеры процессов. Эта ошибка исправлена, но с тех пор у меня был включен столбец обработчиков в диспетчере задач Windows, просто на случай, если я столкнусь с другой утечкой обработчиков.

Из-за этой обычной проверки в феврале 2021 года я заметил, что в одном из процессов Chrome было открыто более 20 000 дескрипторов!

Сейчас эта ошибка в Chrome исправлена, но я хотел поделиться тем, как исследовать утечки хэндлов, потому что существуют и другие программы с утечками. Я также хотел поделиться своим процессом обучения.

Небольшое исследование с помощью диспетчера задач Chrome показало, что процесс, о котором идет речь, был процессом рендеринга для gmail, а еще небольшое исследование показало, что большинство процессов рендеринга Chrome имеют менее 1 000 дескрипторов ядра.

20 000 показались аномальными, и после мониторинга в течение нескольких часов я увидел, что количество хэндлов бесконечно увеличивается.
Пощадите живых, похоже, у нас утечка хэндлов.

Медленные шаги к решению

Первое, что я хотел узнать, это что это за дескрипторы . Дескрипторы Windows могут относиться к файлам, процессам, потокам, событиям, семафорам и многим другим объектам ядра. Я обратился к инструменту дескрипторов sysinternals, чтобы посмотреть, какой это тип, но он сказал, что в этом процессе было открыто всего несколько сотен дескрипторов. Оказывается, что handle.exe по умолчанию отображает только информацию о дескрипторах файлов. Чтобы получить полную информацию, вы можете передать -a, чтобы вывести информацию обо всех дескрипторах, или -s, чтобы подвести итог по типу. Параметр -s наиболее полезен для этого исследования. Типичный вывод выглядел для gmail следующим образом (запуск его из командной строки администратора разрешил бы дескрипторы <Неизвестный тип>):

Код:
>handle -s -p 13360

Nthandle v4.22 – Handle viewer
Copyright (C) 1997-2019 Mark Russinovich
Sysinternals – http://www.sysinternals.com

Handle type summary:
  <Unknown type>  : 4
  <Unknown type>  : 77
  <Unknown type>  : 48
  ALPC Port       : 1
  Directory       : 2
  Event           : 20858
  File            : 147
  IoCompletion    : 4
  IRTimer         : 6
  Key             : 8
  Semaphore       : 8
  Thread          : 31
  TpWorkerFactory : 3
  WaitCompletionPacket: 10
Total handles: 21207


Итак. События обрабатываются. Могло быть и хуже.
Утечка хэндлов процессов хранит вокруг себя дорогостоящие структуры ядра, которые, похоже, составляют около 64 КБ на хэндл.
Дескрипторы событий, по сравнению с этим, довольно дешевы - возможно, шестнадцать байт или около того (это трудно измерить).
Вполне возможно, что влияние этой утечки было незначительным. Но Chrome использует объекты RAII для управления всеми своими хэндлами, поэтому у нас не должно быть утечек - я хотел понять, что происходит не так.

Немного поспрашивав, я обнаружил, что никто из моих коллег не знает, как исследовать утечки данных, поэтому мне пришлось самому разбираться. Я подключил отладчик к процессу gmail и попробовал установить точку останова на функции, создающей события, но получил слишком много шума. Создавались сотни событий, и более 99% из них удалялись. Мне нужно что-то получше.

ETW

Кто-то может сказать, что я слишком увлечен использованием Event Tracing for Windows, пытаясь решить все проблемы, но пока она работает, я буду продолжать ее использовать. Я подозревал, что ETW может отслеживать создание и уничтожение ручек и находить утечки, но не знал, как это сделать.

Немного поискав в Google, я нашел эту статью. Она помогла мне начать, но в ней есть несколько недостатков:

  • В статье показаны десятки флагов, передаваемых в xperf.exe, но нет объяснения, что это такое.
  • Указанные флаги приводили к очень высокой скорости передачи данных, что делало длительную трассировку нецелесообразной.
  • В сообщении было обещано объяснить, как анализировать данные в "следующем сообщении", но никакого "следующего сообщения" не было, а обращение за помощью ни к чему не привел

Недостатки или нет, но это позволило мне разобраться в деле. Я начал с того, что использовал его в основном как есть. Я написал тестовую программу, которая сливала 10 000 хэндлов, и записал трассировку, чтобы посмотреть, смогу ли я увидеть намеренную утечку. Это сработало.

Однако она записывала данные со скоростью много гигабайт в час. Поскольку утечка хэндлов происходила довольно медленно - для утечки 20 000 хэндлов потребовались недели - мне нужна была многочасовая трассировка, чтобы быть уверенным, что я смогу поймать утечку. Я сосредоточился на двух стратегиях:

Уменьшим объем записываемых данных

Исходные данные трассировки записаны от этих поставщиков:

Proc_Thread+Loader+Latency+DISPATCHER+ob_handle+ob_object

Вы можете узнать больше о том, что некоторые из них означают, посмотрев на вывод xperf -providers, но краткая версия такова: рекомендуемая командная строка не просто записывает информацию о хэндлах, она также записывает информацию о каждом переключении контекста и всех дисковых вводах/выводах (см. Latency), включает профилировщик выборки (также в Latency) и многое другое. Моим первым шагом было сократить ее до этого:


PROC_THREAD+LOADER+OB_HANDLE+OB_OBJECT

PROC_THREAD+LOADER необходим, чтобы понять смысл любой трассировки ETW, а OB_HANDLE+OB_OB_OBJECT казались критическими для трассировки хендлеров. Затем представитель Microsoft сказал мне, что OB_OBJECT не нужен, поэтому я его удалил. Рекомендуемая команда также имела шесть различных параметров для флага -stackwalk, когда мне действительно нужны только стеки для выделения хэндлов. Я удалил пять из них, а затем добавил HandleDuplicate на всякий случай.

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

xperf.exe -start -on PROC_THREAD+LOADER+OB_HANDLE -stackwalk HandleCreate+HandleDuplicate

Вы можете увидеть более полнофункциональную командную строку в пакетном файле на github, но здесь показана минимальная идея. Она начинает трассировку, записывает достаточно информации, чтобы отнести события к нужному потоку и модулю (PROC_THREAD+LOADER), записывает информацию об операциях с хэндлами (OB_HANDLE) и записывает стеки вызовов при создании и дублировании хэндлов. Отлично!

Уменьшм объем генерируемых данных

Затем я загрузил (все еще слишком большие) следы в WPA и посмотрел, откуда поступают все данные. Процессами, которые генерировали больше всего событий, были Task Manager и Chrome Remote Desktop. Большое количество трафика означало, что эти два процесса создавали и уничтожали большое количество дескрипторов. Обычно это не было проблемой (они не пропускали хэндлы), но это увеличивало размер трассировки, поэтому решение было очевидным - отключить их на время трассировки. Закрыть диспетчер задач было достаточно просто, но поскольку я проводил это исследование из дома на своей рабочей станции в офисе, закрывать Chrome Remote Desktop было менее удобно. Но, не беспокойтесь. Я убедился, что все работает правильно, затем отключился (чтобы уменьшить объем генерируемых данных) и снова подключился через двенадцать часов. Подобная тактика - закрытие программ, генерирующих чрезмерное количество данных, - часто бывает полезна при использовании ETW, поскольку он обычно записывает информацию обо всей системе, включая все запущенные процессы.

Анализ​

Теперь у меня было несколько больших, но управляемых трасс (~970 МБ, сжатых), и я мог начать анализ.
Я загрузил трассировку в Windows Performance Analyzer и в итоге нашел график отслеживания обработок. Он находится в Graph Explorer, в разделе Memory (???), Handles, Outstanding Count by Process. Отлично!

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

image_thumb.png


График был красивым, но я решил, что мне нужны необработанные цифры, поэтому я посмотрел на таблицу.
Стандартное представление выглядело довольно плачевно для процесса браузера Chrome: было выделено почти 1,5 миллиона хендлеров. Но подождите...

image_thumb-1.png


Утечка хэндлов произошла не в процессе браузера, и она не приближалась к 1,5 миллионам хэндлов. Оказалось, что представление по умолчанию вводит в заблуждение. Хотя график/таблица называются "Outstanding Count by Process", и это действительно то, что показывает график, таблица на самом деле показывает кумулятивный подсчет по процессам. То есть, процесс браузера выделил 1,5 миллиона хэндлов за двенадцать часов, но он освободил практически все из них, так что кого это волнует?

Что я хотел увидеть, так это оставшиеся хэндлы - выделенные, но не освобожденные. Таблица может показать эти данные, но это не так просто.

В таблицах трассировки кучи есть столбец с надписью Type. Это уникально неудачный выбор для названия столбца, но это очень важный столбец. Очень важно, что в контексте столбца Type "Inside" означает внутри временного диапазона, регистрируемого трассировкой, а "Outside" означает вне временного диапазона, регистрируемого трассировкой. Доступными типами являются:

  • AIFI (Allocated Inside Freed Inside - не утечка)
  • AOFO (Allocated Outside Freed Outside - кто знает!)
  • AOFI (Выделено снаружи, освобождено внутри - не утечка)
  • AIFO (Выделено внутри освобожденного снаружи - потенциальная утечка!)
AIFO - это интересные события, потому что они были выделены во время записи трассировки, а затем не освобождены до остановки записи трассировки. Они могли быть освобождены несколько секунд спустя, но если вы видите их достаточно много в одном и том же стеке вызовов, вы начинаете подозревать...

Так работает трассировка кучи. Трассировка кучи взяла эту концепцию и изменила ее настолько, чтобы запутать.
  1. Они переименовали колонку с Type на Lifetime. Lifetime - определенно лучшее название, но неизбежно возникает путаница из-за наличия двух названий для одного и того же понятия.
  2. Они сделали столбец отключенным по умолчанию. Это соответствует трассировке кучи, в которой столбец Type по умолчанию выключен, но это необъяснимо. Это единственный наиболее важный столбец в таблице, поэтому я не понимаю, почему он не находится в центре внимания.
  3. В дополнение к тому, что по умолчанию столбец выключен, они фактически скрывают его! Обычно вы можете щелкнуть правой кнопкой мыши на заголовке любого столбца, выбрать "Больше столбцов..." и увидеть список столбцов, которые вы можете включить. Многие пользователи (я в том числе), вероятно, полагают, что список колонок в меню является полным. Это не так. Для некоторых колонок - включая жизненно важную колонку "Срок службы" - необходимо вызвать редактор представлений (Ctrl+E или щелкнуть по шестеренке справа от "Outstanding Count by Process"), а затем перетащить колонку "Срок службы" из списка Available Columns слева в список справа. Вздох...

Я сообщил об этих проблемах руководителю WPA, и они согласились с необходимостью улучшений. Теперь, когда WPA (Preview) доступен в магазине Microsoft, следует ожидать, что подобные исправления будут поставляться быстрее, чем когда-либо прежде.

Последняя проблема, на которую следует обратить внимание при трассировке рукояток ETW, заключается в том, что в ней, похоже, есть некоторые ошибки учета. Он будет сообщать об утечках, которые невозможны, учитывая вывод "handle -s", поэтому обязательно перепроверьте результаты, прежде чем тратить слишком много времени.

Поиск бага

После всего этого обучения и открытий я пришел к такому виду. Оно показывало 527 обработчиков событий, утеченных процессом gmail за двенадцать часов, причем 510 из них были утечками в одном и том же стеке вызовов, заканчивающемся конструктором WaitableEvent:

image_thumb-2.png


Текстовая версия стека ключевых вызовов находится здесь:

Код:
base::`anonymous namespace’::ThreadFunc
blink::scheduler::WorkerThread::SimpleThreadImpl::Run
base::RunLoop::Run
base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run
base::MessagePumpDefault::Run
base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork
base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl
base::TaskAnnotator::RunTask
blink::WorkerThread::InitializeSchedulerOnWorkerThread
blink::scheduler::WorkerScheduler::WorkerScheduler
blink::scheduler::NonMainThreadSchedulerImpl::CreateTaskQueue
blink::scheduler::NonMainThreadSchedulerHelper::NewTaskQueue
base::sequence_manager::internal::SequenceManagerImpl::CreateTaskQueueImpl
base::sequence_manager::internal::TaskQueueImpl::TaskQueueImpl
base::internal::OperationsController::OperationsController
base::WaitableEvent::WaitableEvent
KernelBase.dll!CreateEventW

Объект события выделялся и хранился в классе WaitableEvent в Chrome, управляемом переменной-членом ScopedHandle. Это означало, что если мы пропускаем эти хендлеры, то пропускаем и объекты WaitableEvent. Если мы пропускаем эти объекты, то что еще мы пропускаем?

Я использовал трассировку моментальных снимков кучи для наблюдения за процессом gmail. Для меня это была знакомая территория, поэтому все прошло гладко. Затем я поискал потенциальные утечки в стеках вызовов, аналогичные утечке handle, и нашел несколько. Затем я осмотрелся в поисках других потенциальных утечек с похожими стеками вызовов, которые выглядели правдоподобно связанными, и нашел набор, который казался связанным с утечками объектов IDBFactory и OperationsController.

Я использовал инструменты разработчика Chrome, чтобы проверить, не происходит ли в gmail утечка объектов, связанных с IDB. Это было не так, поэтому я не смог обвинить в этом команду gmail.

Сотрудничество

В итоге несколько человек внесли свой вклад в анализ. Я понимал, как интерпретировать трассировки, и мог воспроизвести утечки на пользовательских инструментальных версиях Chrome, но я ужасно разбираюсь в JavaScript и не понимаю архитектуру нашей IDB. Один из моих коллег понял архитектуру утечки объектов. Этот коллега понял, что утечка выделений происходила в потоках рабочих служб, и утечка происходила всякий раз, когда потоки рабочих служб уходили, пока было открыто соединение с IndexedDB. Оказалось, что это происходит довольно регулярно на некоторых веб-сайтах, и Chrome плохо справлялся с этим. Это и есть ошибка!

Дальнейшее расследование подтвердило эту теорию - увеличение количества просочившихся объектов коррелировало с завершением потоков рабочих служб. Затем я понял, что рабочие службы были связаны с автономным режимом Google Drive. Если я отключал автономный режим, то утечки исчезали.

На понимание ошибки ушли месяцы, но затем исправление было создано довольно быстро (не мной - я до сих пор не понимаю эту часть кода) и через несколько дней появилось в репозитории Chromium.

Исправление появилось в мае в версии M92, которая начала распространяться среди обычных пользователей примерно 21 июля 2021 года. Если вы еще не получили исправление, то скоро получите.

Стресс-тестирование

Когда ошибка была понята, я смог провести стресс-тесты. Используя версию Chrome с ошибкой, я оставил открытыми одновременно gmail, sheets, docs и drive. Все эти приложения используют автономный режим, поэтому все они работали в автономном режиме. Окно Chrome выглядело следующим образом:

image_thumb-3.png


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

image_thumb-4.png


Пять из одиннадцати верхних процессов - это chrome.exe (четыре рендера плюс процесс браузера).
Как только я обновил браузер, четыре рендерера больше не отображаются в верхнем списке. Но что насчет других процессов?

  1. WPA.exe - это Windows Performance Analyzer - инструмент, который я использую для анализа следов утечки хэндла. Он оставляет открытыми около 1700 файлов .symcache и, похоже, имеет утечку обработчика событий, как и Chrome. Я сообщил об этих проблемах разработчику, и они проводят расследование.
  2. В системе есть смесь обработчиков событий, обработчиков файлов, обработчиков IoCompletion, обработчиков процессов, обработчиков WaitCompletionPacket и других. Я не знаю, представляют ли они собой утечки. Полагаю, что нет.
  3. Эта копия dllhost.exe содержит thumbcache.dll. Небольшие эксперименты показали, что если я создаю новые файлы .mp4, то этот процесс пропускает 12+ обработчиков WaitCompletionPacket на файл, когда проводник создает эскизы. Когда я очищаю видео из отпуска, это может привести к утечке сотен или тысяч хэндлов. Я упоминал об этом в Твиттере дважды. Я также сообщил об этом на Feedback Hub (только для некорпоративных пользователей Windows 10). Трассировку ETW, показывающую создание 20 файлов .mp4 и утечку сотен ручек, можно найти здесь. Это должно быть исправлено.
  4. Последним является IntelTechnologyAccessService.exe, который сливает дескрипторы событий. Без символов или какого-либо представления о том, что он делает, я могу только сказать, что утечки происходят из его core.dll. Я написал об этой утечке в Твиттере и получил быстрый ответ от разработчика Intel, который хочет исправить утечку.

Я провел тот же стресс-тест как с отключенным автономным режимом, так и с исправленной версией Chrome (с вновь включенным автономным режимом). Это показало, что отключение автономного режима остановило появление ошибки и доказало, что исправление работает. Утечки хендлеров исчезли, как и связанные с ними утечки памяти.

При запуске исправленной версии Chrome диспетчер задач Windows выглядел примерно так.
image_thumb-5.png


Процессы браузера и GPU Chrome (не показаны) по-прежнему имеют более 1000 открытых обработчиков, что объясняется их работой, но процессы рендеринга используют достаточно мало обработчиков, чтобы ни один из них больше не попал на первую страницу результатов - проблема решена. dllhost.exe поднялся в рейтинге, потому что я протестировал его, создав 512 новых файлов .mp4, а explorer появился, потому что, очевидно, тестирование .mp4 выявило в нем утечку обработчиков событий. Вздох...

Выводы

Утечки обработчиков - не самая страшная вещь на свете. Утечки дескрипторов процессов стоят довольно дорого, а утечки дескрипторов событий - нет. За исключением того, что в некоторых случаях - например, в Chrome - утечка хэндла может коррелировать с другими утечками, поэтому их обычно стоит расследовать.

Обратите внимание на диспетчер задач, особенно на свои собственные процессы, чтобы избежать этой проблемы.

Я до сих пор не знаю, как исследовать утечки хендлеров GDI - если кто-то знает, пожалуйста, дайте мне знать.

Вы можете прочитать многомесячное расследование - ошибки и все такое - в баге Chromium.

Вот объявление в твиттере, обсуждение в новостях хакеров и обсуждение на reddit.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх