- PVSM.RU - https://www.pvsm.ru -

«Эй ты, функция. Да, я к тебе обращаюсь. При очистке не забудь, пожалуйста, восстановить все мои регистры. Да, и этот тоже, ты что, думаешь, в Linux попала?»
Вот краткое описание проблемы, с которой я столкнулся. ABI (Application Binary Interface [1]) платформы требует от функций, чтобы они сохраняли значения определённых регистров и восстанавливали их в случае использования, однако набор восстанавливаемых регистров зависит от платформы, и правила в Linux отличаются от правил в Windows. Возможно, поэтому я столкнулся с повреждением регистров Chrome в Windows. Но давайте начнём с самого начала.
Меня попросили изучить баг вылета [2] в Chrome. Вылет чётко коррелировал с инъецированием сторонних DLL в процессы Chrome (а их мы не можем поддерживать и не поддерживаем), поэтому была высока вероятность того, что причиной стали эти сторонние DLL, но мне всё равно хотелось понять, что же происходит.
Мои коллеги исследовали этот баг ранее и добавили несколько дополнительных тестов, поэтому вылет был изолирован до этого псевдокода (настоящий код находится здесь [3]):
while (StillRunning()) {
DoLotsOfStuff();
ImportantFunction(std::move(m_ptr));
CHECK(!m_ptr);
}
Показанный выше код представляет собой долго работающий цикл, выполняющий множество действий. В конце каждой итерации он вызывает функцию и перемещает умный указатель на параметры функции, что должно обнулять указатель. Вылет происходил, когда конструкция CHECK замечала, что указатель на источник на самом деле не был обнулён. В таком случае мы намеренно устраивали вылет, чтобы избежать повреждения памяти.
Изучить этот баг меня заставило любопытное поведение последних двух строк. Как мы можем обнулить указатель в одной строке кода, а потом в следующей строке обнаружить, что он ненулевой? Даже если виновники — сторонние DLL, как им удаётся это сделать? Я проверил наличие модификаций байтов кода рядом с вылетом и ничего не нашёл, так как же? Мне хотелось это понять.
Как обычно, я скачал один из дампов вылетов и изучил ассемблерные команды, реализующие исходный код на C++. Ниже в смеси псевдокода и ассемблера показано то, во что транслировался код (подробности см. в комментарии 61 [4] к багу):
xorps xmm7, xmm7 ; Zero register xmm7
while (StillRunning()) {
DoLotsOfStuff();
mov rax, QWORD PTR[rsp + 50h]
movaps QWORD PTR[rsp + 50h], xmm7 ; zero m_ptr
call ImportantFunction
CrashIfNonZero(rsp+50h);
}
Простите за перемешивание метафор; суть в том, что перед запуском цикла компилятор решил обнулить регистр XMM7 (один из регистров SSE [5]). Затем в конце каждой итерации цикла он использует XMM7 для обнуления m_ptr (хранящегося по адресу rsp+50h). Компилятор ожидал, что XMM7 останется обнулённым, но это было не так.
Я изучил большое количество дампов вылетов, чтобы посмотреть, есть ли какой-то паттерн в значениях внутри XMM7. Вот четыре из найденных мной значений:
Если в этих числах и есть паттерн, то я определённо его не вижу. Случайность — это ещё одна улика, ограничивающая список возможных источников проблем.

Функции DoLotsOfStuff и ImportantFunction, а также все функции, которые они вызывают, в соответствии с требованиями Windows ABI [6], обязаны сохранять XMM7 (в Linux такого требования нет). Если они используют его, то обязаны его восстановить. Но одна из них этого не делала (или повреждалось место в стеке, где они хранились, но это кажется менее вероятным). В большинстве вылетов в процессе Chrome присутствовали сторонние DLL. Предположительно, эти DLL должны выполнять перехват функций Chrome или операционной системы, а их инъецируемый код, предположительно, повреждал XMM7.
Я написал об этом твит [7], пытаясь узнать теории о том, как это могло происходить. Среди прочих ответов с рассуждениями об ISR, DPC и драйверах я увидел ответ от человека, с которым никогда раньше не общался [8]. Если вкратце, он сказал: «А как насчёт этого кода Chromium [9]?»
Я увидел этот твит с моего домашнего ноутбука, а когда пошёл проверить на рабочей машине, автор уже его удалил. Моё любопытство разыгралось, поэтому я написал ему в личку. Он ответил, что код показался ему подозрительным, но потом он понял, что проблему разработчики осознали [10] и что этот код на самом деле не компилируется в Chrome в Windows. В этом и сложность поиска неверного использования XMM7 в исходном коде Chromium — ссылок слишком много (более 17 тысяч), и большинство из них к делу не относится.
Затем он сказал, что перешёл к анализу двоичного файла при помощи IDA Pro
и обнаружил пару функций, попавших в chrome.dll, но не восстанавливавших XMM7. После этого он отправил ссылки на исходный код, который действительно выглядел как реальные баги. Именно в таком случае анализировать двоичные файлы на самом деле проще, чем «читать исходники», потому что в машинном коде все макросы и #ifdef уже обработаны, и в нём видно именно то, что и есть на самом деле.
Я решил воспроизвести его работу при помощи dumpbin /disasm и простого кода на Python для сканирования вывода. Для каждой функции в Chrome (найденной поиском глобальных символов в дизассемблированном выводе) мой скрипт проверял, использовался ли XMM7 без сохранения. Изначально я проверял, записывался ли он относительно rsp перед его первым использованием, но выяснил, что он записывается относительно rax и rbp, поэтому ослабил требования эвристики. Мой скрипт всё равно выдавал ложноположительные срабатывания и мог также выдавать ложноотрицательные, но работал достаточно хорошо, чтобы быть полезным.
Несмотря на первоначальное предположение о том, что баг вызван сторонними разработчиками, мой простой скрипт нашёл множество подозрительных функций. Обнаружилось приблизительно три категории функций, в которых первое использование XMM7 не восстанавливало его:
dav1d_iflipadst_16x8_internal_16bpc_sse4 (отсюда? [11]), являющиеся функциями внутреннего использования для библиотеки dav1d. Все эти функции вызываются обёртками, сохраняющими и восстанавливающими XMM7, то есть с ними всё было в порядке.__longjmp_internal, которые по определению восстанавливали все долговременные регистры, чтобы они могли возвратиться к предыдущему состоянию выполнения.
При помощи этой грубой методики анализа двоичных файлов я в конечном итоге смог найти те же самые забагованные функции в chrome.dll, которые обнаружил мой собеседник в Twitter.
Функция ScaleRowUp2_Bilinear_12_SSSE3 [12] в WebRTC записывала в XMM7 константу 0x0008000800080008 без предварительного сохранения. Это баг, и он может вызывать вылеты, но я знал, что он не был причиной этого вылета, поскольку наблюдавшиеся мной значения XMM7 были сильно случайными. Я отправил отчёт о проблеме автору, он зарегистрировал баг [13] и устранил его в течение 24 часов.
DyadicBilinearQuarterDownsampler_sse [14] в openh264 тоже использовала XMM7 без его сохранения. Видеокодеки часто обрабатывают значения с высокой энтропией, поэтому возможно это могло создавать виденные мной случайные значения (спойлер: причина была не в этом) и это определённо было неправильно. Я зарегистрировал баг [15], а затем решил устранить его. Внедрение этого исправления вызвало пару сложностей:
Проблемы WebRTC и openh264 были настоящими багами, а их устранение, вероятно, предотвратит будущие вылеты в Chromium, однако они никак не затрагивали исследуемый мной баг. Вылеты продолжались. По-прежнему наиболее вероятным объяснением было стороннее ПО.

Было множество намёков на то, какой тип стороннего ПО может быть проблемой. Это было нечто, создающее данные с высокой рандомизацией. Существовала очевидная корреляция со сторонним ПО шифрования диска. Один пользователь, с которым я исследовал вылеты, использовал сторонний продукт для шифрования диска, а Microsoft заметила корреляцию с задачами, заставляющими работать файловую систему [26]. Были предприняты попытки связаться с поставщиком ПО.
Мы связались с поставщиком (McAfee/Trellix) и он выпустил исправление для продукта Drive Encryption [27].
Я рад, что первопричина была устранена, но ещё бы мне хотелось, чтобы разработчики, работающие над продуктом, в котором используется язык ассемблера, могли выполнять аудит своего кода, чтобы убедиться, что он соответствует требованиям Windows ABI. Это не первый случай такого класса багов [28] и определённо не последний.
Я решил написать эту статью, потому что мне показалось, что это приключение было интересным, но ещё и потому, что оно ещё не закончено. Могут быть и другие регистры, которые неправильно сохраняются и восстанавливаются в Chromium. Могут существовать другие проекты [29], делающие эту ошибку, иногда незнакомые с различиями между ABI Linux и Windows. Любые правила ПО, которые не тестируются и не применяются принудительно, неизбежно будут сломаны, а мне неизвестны способы структурированного тестирования для выявления нарушений ABI. Похоже, появление новых багов этого типа неизбежно.
Эти вылеты начали происходить примерно с версии M91 браузера Chrome. Поначалу они выглядели как баг Chrome, но теперь кажется, что больше вероятность того, что компилятор или код Chromium изменился так, что стал уязвим к повреждению регистра XMM7, которое и так уже происходило в экосистеме. До M91 браузер Chrome вообще не использовал XMM7 в функции RunWorker (я проверял [30]), а начиная с M91 генерация кода изменилась (смена компилятора?) и функция начала полагаться на то, что XMM7 часами оставался обнулённым. Поэтому пожалуйста, восстанавливайте регистры, завершив с ними работу.
И снова спасибо Dougall [8] за демонстрацию проблемы и за то, что вдохновил меня изучить её глубже.
Автор:
PatientZero
Источник [31]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/381184
Ссылки в тексте:
[1] Application Binary Interface: https://en.wikipedia.org/wiki/Application_binary_interface
[2] баг вылета: https://bugs.chromium.org/p/chromium/issues/detail?id=1218384
[3] здесь: https://source.chromium.org/chromium/chromium/src/+/main:base/task/thread_pool/worker_thread.cc;l=481?q=base::internal::WorkerThread::RunWorker
[4] комментарии 61: https://bugs.chromium.org/p/chromium/issues/detail?id=1218384#c61
[5] регистров SSE: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions#Registers
[6] требованиями Windows ABI: https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170
[7] написал об этом твит: https://twitter.com/BruceDawson0xB/status/1582461778361217025
[8] человека, с которым никогда раньше не общался: https://twitter.com/dougallj
[9] этого кода Chromium: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/boringssl/win-x86_64/crypto/cipher_extra/aes128gcmsiv-x86_64.asm?pli=1#3142
[10] разработчики осознали: https://boringssl.googlesource.com/boringssl/+/master/crypto/cipher_extra/e_aesgcmsiv.c?pli=1#30
[11] отсюда?: https://source.chromium.org/chromium/chromium/src/+/main:third_party/dav1d/libdav1d/src/x86/
[12] ScaleRowUp2_Bilinear_12_SSSE3: https://source.chromium.org/chromium/chromium/src/+/main:third_party/libyuv/source/scale_gcc.cc;l=1003?q=ScaleRowUp2_Bilinear_12_SSSE3
[13] зарегистрировал баг: https://bugs.chromium.org/p/libyuv/issues/detail?id=945
[14] DyadicBilinearQuarterDownsampler_sse: https://source.chromium.org/chromium/chromium/src/+/main:third_party/openh264/src/codec/processing/src/x86/downsample_bilinear.asm;l=2226?q=DyadicBilinearQuarterDownsampler_sse
[15] зарегистрировал баг: https://github.com/cisco/openh264/issues/3585
[16] исправление в две строки: https://github.com/cisco/openh264/commit/db956674bbdfbaab5acdd3fdb4117c2fef5527e9
[17] WebRTC: https://webrtc.org/
[18] один: https://chromium-review.googlesource.com/c/chromium/src/+/3986032
[19] два: https://webrtc-review.googlesource.com/c/src/+/280942
[20] три: https://webrtc-review.googlesource.com/c/src/+/280800
[21] четыре: https://chromium-review.googlesource.com/c/chromium/src/+/3996007
[22] пять: https://chromium-review.googlesource.com/c/chromium/src/+/3978910
[23] шесть: https://webrtc-review.googlesource.com/c/src/+/281561
[24] семь: https://chromium-review.googlesource.com/c/chromium/src/+/4027295
[25] восемь: https://chromium-review.googlesource.com/c/chromium/src/+/4028582
[26] корреляцию с задачами, заставляющими работать файловую систему: https://bugs.chromium.org/p/chromium/issues/detail?id=1218384#c78
[27] выпустил исправление для продукта Drive Encryption: https://bugs.chromium.org/p/chromium/issues/detail?id=1218384#c93
[28] первый случай такого класса багов: https://twitter.com/Zalathar/status/1595204081429012480
[29] другие проекты: https://twitter.com/Zalathar/status/1594970774603202561
[30] я проверял: https://bugs.chromium.org/p/chromium/issues/detail?id=1218384#c92
[31] Источник: https://habr.com/ru/post/703894/?utm_source=habrahabr&utm_medium=rss&utm_campaign=703894
Нажмите здесь для печати.