Анализ первопричин ошибки или уязвимости Maglev Graph Builder?!

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Введение

Maglev — это новый оптимизирующий компилятор, представленный в Chrome M117. Для получения подробной информации о его истории, производительности (по сравнению со Sparkplug и Turbofan) и его подходе, основанном на статических единичных назначениях, вы можете обратиться к этой статье - https://v8.dev/blog/maglev, опубликованной в начале декабря этого года. Мы сосредоточимся на анализе первопричин уязвимости, обнаруженной фаззером.

Фаззер

Благодаря Fuzzilli - https://github.com/googleprojectzero/fuzzilli, разработанному и поддерживаемому Сэмюэлем Гроссом и Карлом Смитом из Google V8 Security, мы начали фаззинг уязвимостей Chrome V8 путем модификации фаззера, включая настройку вероятности генерации различных вызовов функций, изучение опубликованных уязвимостей и чтение исходного кода и создание шаблонов.

Открытие

Мы проверили версию V8 с хешем (a8275630c081e7842e05ae7fecc4d3e85888a4e6) и определили следующее доказательство концепции (PoC) и информацию о сбое, как показано ниже. В сообщении о сбое проверки отладки отображается label->predecessor_count_ > 1 с фатальной ошибкой в строке 515 ../../src/maglev/maglev-graph-builder.cc.

function f0(a1, a2, a3, a4) {
return a3;
}
const v6 = Array();
function f7(a8) {
a8[1073741824] = a8;
return f7;
}
const t8 = f7(v6);
t8(f0);
%OptimizeMaglevOnNextCall(f7);
f7(v6);

// CRASH INFO
// ==========
// TERMSIG: 6
// STDERR:
// #
// # Fatal error in ../../src/maglev/maglev-graph-builder.cc, line 515
// # Debug check failed: label->predecessor_count_ > 1 (1 vs. 1).
// #
// #
// #
// #FailureMessage Object: 0x7fff334abbd0
// ==== C stack trace ===============================
//
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x8958f2) [0x55cc4897e8f2]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x894137) [0x55cc4897d137]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x88656f) [0x55cc4896f56f]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x885f25) [0x55cc4896ef25]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x2120067) [0x55cc4a209067]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x21dcceb) [0x55cc4a2c5ceb]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x214cc9c) [0x55cc4a235c9c]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1f39ae0) [0x55cc4a022ae0]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1f355df) [0x55cc4a01e5df]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1f315f0) [0x55cc4a01a5f0]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1f2fc9f) [0x55cc4a018c9f]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1f28579) [0x55cc4a011579]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0xb6eb22) [0x55cc48c57b22]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0xb9bbe1) [0x55cc48c84be1]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0xb823c2) [0x55cc48c6b3c2]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0xb84bf1) [0x55cc48c6dbf1]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1d7e9e3) [0x55cc49e679e3]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x1d7e21c) [0x55cc49e6721c]
// /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8(+0x3f36176) [0x55cc4c01f176]
// Received signal 6
// STDOUT:
// [COV] edge counters initialized. Shared memory: shm_id_103962_2 with 1404120 edges
// FUZZER ARGS: ../.build/x86_64-unknown-linux-gnu/release/FuzzilliCli --profile=v8 /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8 --storagePath=/home/fuzz/v8_crash_maglev_master_20231216 --engine=hybrid --jobs=5 --resume
// TARGET ARGS: /home/fuzz/Fuzzing_v8/v8/out/fuzzbuild/d8 --expose-gc --omit-quit --allow-natives-syntax --fuzzing --jit-fuzzing --future --harmony --no-turbofan --no-turboshaft
// CONTRIBUTORS: ExplorationMutator, FixupMutator
// EXECUTION TIME: 26ms
Нажмите, чтобы раскрыть...

Трудно определить, что произошло и причину уязвимости, поэтому переходим к отладке с помощью GDB.

MaglevGraphBuilder

Перед этим дополняем описанием MaglevGraphBuilder. MaglevGraphBuilder отвечает за :

1. Преобразование байт-кода в граф узлов, представляющий операции программы.

2. Выполнение оптимизации на этом графе для улучшения производительности кода во время выполнения.

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

Таким образом, maglev-graph-builder.h содержит объявления и определения классов и утилит, которые являются частью JIT-компилятора Maglev V8. Эти компоненты помогают создавать и оптимизировать внутреннее представление кода JavaScript для быстрого выполнения.

Как показано на снимке экрана ниже, видно, что в № 5 и № 6 он вызывает функцию «ReducePredecessorCount» и впоследствии вызывает фатальную ошибку в № 3.

1708003868004.png



[#5] 0x555557674067 → v8::internal::maglev::MaglevGraphBuilder::MaglevSubGraphBuilder::ReducePredecessorCount(v8::internal::maglev::MaglevGraphBuilder::MaglevSubGraphBuilder::Label*)()

[#6] 0x5555577312b7 → v8::internal::maglev::ReduceResult v8::internal::maglev::MaglevGraphBuilder::tryBuildPolymorphicElementAccess<v8::internal::maglev::MaglevGraphBuilder::VisitSetKeyedProperty()::$_0&>(v8::internal::maglev::ValueNode*, v8::internal::maglev::ValueNode*, v8::internal::compiler::KeyedAccessMode const&, v8::internal::ZoneVector<v8::internal::compiler::ElementAccessInfo> const&, v8::internal::maglev::MaglevGraphBuilder::VisitSetKeyedProperty()::$_0&)()
Нажмите, чтобы раскрыть...

Каково количество предшественников? Как это следует рассчитывать?

В файле Maglev-Graph-Builder.h есть функция CalculatePredecessorCounts :

C++: Скопировать в буфер обмена
Код:
void CalculatePredecessorCounts() {
    // Add 1 after the end of the bytecode so we can always write to the offset
    // after the last bytecode.
    size_t array_length = bytecode().length() + 1;
    predecessors_ = zone()->AllocateArray<uint32_t>(array_length);
    MemsetUint32(predecessors_, 0, entrypoint_);
    MemsetUint32(predecessors_ + entrypoint_, 1, array_length - entrypoint_);

// We count jumps from peeled loops to outside of the loop twice.
    bool is_loop_peeling_iteration = false;
    base::Optional<int> peeled_loop_end;
    interpreter::BytecodeArrayIterator iterator(bytecode().object());
    for (iterator.SetOffset(entrypoint_); !iterator.done();
         iterator.Advance()) {
      interpreter::Bytecode bytecode = iterator.current_bytecode();
      if (allow_loop_peeling_ &&
          bytecode_analysis().IsLoopHeader(iterator.current_offset())) {
        const compiler::LoopInfo& loop_info =
            bytecode_analysis().GetLoopInfoFor(iterator.current_offset());
        // Generators use irreducible control flow, which makes loop peeling too
        // complicated.
        if (loop_info.innermost() && !loop_info.resumable() &&
            (loop_info.loop_end() - loop_info.loop_start()) <
                v8_flags.maglev_loop_peeling_max_size &&
            (!v8_flags.maglev_loop_peeling_only_trivial ||
             loop_info.trivial())) {
          DCHECK(!is_loop_peeling_iteration);
          is_loop_peeling_iteration = true;
          loop_headers_to_peel_.Add(iterator.current_offset());
          peeled_loop_end = bytecode_analysis().GetLoopEndOffsetForInnermost(
              iterator.current_offset());
        }
      }
      if (interpreter::Bytecodes::IsJump(bytecode)) {
        if (is_loop_peeling_iteration &&
            bytecode == interpreter::Bytecode::kJumpLoop) {
          DCHECK_EQ(iterator.next_offset(), peeled_loop_end);
          is_loop_peeling_iteration = false;
          peeled_loop_end = {};
        }
        if (iterator.GetJumpTargetOffset() < entrypoint_) {
          static_assert(kLoopsMustBeEnteredThroughHeader);
          if (predecessors_[iterator.GetJumpTargetOffset()] == 1) {
            // We encoutered a JumpLoop whose loop header is not reachable
            // otherwise. This loop is either dead or the JumpLoop will bail
            // with DeoptimizeReason::kOSREarlyExit.
            predecessors_[iterator.GetJumpTargetOffset()] = 0;
          }
        } else {
          predecessors_[iterator.GetJumpTargetOffset()]++;
        }
        if (is_loop_peeling_iteration &&
            iterator.GetJumpTargetOffset() >= *peeled_loop_end) {
          // Jumps from within the peeled loop to outside need to be counted
          // twice, once for the peeled and once for the regular loop body.
          predecessors_[iterator.GetJumpTargetOffset()]++;
        }
        if (!interpreter::Bytecodes::IsConditionalJump(bytecode)) {
          predecessors_[iterator.next_offset()]--;
        }
      } else if (interpreter::Bytecodes::IsSwitch(bytecode)) {
        for (auto offset : iterator.GetJumpTableTargetOffsets()) {
          predecessors_[offset.target_offset]++;
        }
      } else if (interpreter::Bytecodes::Returns(bytecode) ||
                 interpreter::Bytecodes::UnconditionallyThrows(bytecode)) {
        predecessors_[iterator.next_offset()]--;
        // Collect inline return jumps in the slot after the last bytecode.
        if (is_inline() && interpreter::Bytecodes::Returns(bytecode)) {
          predecessors_[array_length - 1]++;
          if (is_loop_peeling_iteration) {
            predecessors_[array_length - 1]++;
          }
        }
      }
      // TODO(leszeks): Also consider handler entries (the bytecode analysis)
      // will do this automatically I guess if we merge this into that.
    }
    if (!is_inline()) {
      DCHECK_EQ(0, predecessors_[bytecode().length()]);
    }
  }

Функция CalculatePredecessorCounts отвечает за анализ массива байт-кодов JavaScript и вычисление того, сколько раз каждый байт-код используется операцией потока управления, например переходом, переключением или возвратом. Эта информация обычно используется для анализа потока управления, который является важной частью оптимизации компиляторов, таких как Maglev V8.

Ниже приведено пошаговое описание функции CalculatePredecessorCounts :

Инициализация: массив predecessors_ выделяется и инициализируется для хранения количества предшественников для каждого байт-кода. Размер массива на единицу больше длины массива байт-кода для обработки крайних случаев.

Обнаружение очистки цикла. Очистка цикла — это метод оптимизации, при котором первая итерация цикла выполняется отдельно от остальных итераций. Это может сделать тело цикла более рациональным и оптимизировать условные проверки для первой итерации. Функция проверяет заголовки цикла и решает, следует ли его очищать, на основе нескольких условий (например, размера цикла, тривиальности, возможности возобновления).

Перебор байт-кодов. Функция перебирает байт-коды, начиная с точки входа entrypoint_ , используя интерпретатор::BytecodeArrayIterator.

Обработка перехода: если встречается байт-код перехода, функция обновляет массив predecessors_ на основе цели перехода. Специальная обработка применяется к циклам и снятым циклам. Если переход находится внутри очищаемого цикла, счетчик цели увеличивается дважды, чтобы учесть очищенную итерацию.

Обработка переключения: для байт-кодов переключения все целевые смещения в таблице переходов имеют увеличенное число предшественников.

Обработка возврата: байт-коды, которые представляют возвраты или безусловные броски, уменьшают счетчик предшественников для следующего байт-кода. Если функция встроена (is_inline()), она также увеличивает счетчик заполнителя после последнего байт-кода.

Встроенные возвратные переходы: для встроенных функций возвратные переходы собираются в слоте после последнего байт-кода. Если происходит очистка цикла, счетчик увеличивается дважды.

Верифицкация: если функция не встроена, она проверяет, что последний байт-код не имеет предшественников, как и ожидалось.

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

Эта функция является частью процесса оптимизации JIT-компилятора Maglev в движке JavaScript V8. Он используется на этапах компилятора, которые занимаются анализом потока управления для оптимизации выполнения кода. Знание количества предшественников для каждого байт-кода может помочь компилятору принять решения о встраивании, устранении мертвого кода, развертывании цикла и других оптимизациях.

Функция CalculatePredecessorCounts — это пример статического анализа, который выполняют компиляторы, чтобы понять структуру кода и оптимизировать ее перед генерацией машинного кода. Подсчет предшественников имеет решающее значение для построения графа потока управления (CFG), который представляет собой представление всех путей, которые могут пройти через программу во время ее выполнения. Затем CFG используется JIT-компилятором Maglev для создания оптимизированного машинного кода, который эффективно выполняется на ЦП.

Класс ReduceResult

Класс DownloadResult в Maglev-Graph-Builder.h имеет дело с результатом некоторой формы процесса сокращения, который является обычной операцией при оптимизации компилятора, при которой выражения упрощаются или преобразуются.

Вот объяснение класса и его членов:

ReduceResult использует внутреннее перечисление Kind для представления различных состояний результата сокращения. К этим состояниям относятся:

kDoneWithValue: сокращение завершено и дало значение.

kDoneWithAbort : сокращение завершено, но привело к аварийному завершению.

kDoneWithoutValue: сокращение завершено, но не дало значения.

kFail: сокращение не удалось.

kNone: результат сокращения недоступен, что является состоянием по умолчанию.

Класс имеет закрытый член payload_ типа base::pointerWithPayload . Этот тип представляет собой шаблон, который сочетает в себе указатель на ValueNode с полезной нагрузкой Kind в компактном виде. Полезная нагрузка Kind использует 3 бита.

Открытый интерфейс ReduceResult предоставляет несколько конструкторов и методов для создания экземпляров класса и запроса их состояния:

- Конструктор по умолчанию устанавливает состояние kNone .

- Конструктор, принимающий ValueNode*, устанавливает состояние

- kDoneWithValue и гарантирует, что данный ValueNode* не равен нулю.

- Метод value() утверждает, что результат имеет значение ( kDoneWithValue ) и возвращает связанный ValueNode* .

- Методы HasValue(), IsNone(), IsDone(), IsFail(), IsDoneWithValue(), IsDoneWithoutValue() и IsDoneWithAbort() проверяют определенные состояния.

- Методы Done(…), DoneWithAbort() и Fail() — это статические функции, которые создают экземпляры ReduceResult с соответствующими состояниями.

Существуют также перегрузки конструктора копирования и оператора присваивания по умолчанию, обеспечивающие возможность копирования и назначения ReduceResult должным образом.

Этот класс предназначен для использования в процессе компиляции движка JavaScript V8 на этапе оптимизации. Он инкапсулирует результат операции и предоставляет эффективный способ проверить ее результат и получить доступ к любому результирующему значению. Использование base::pointerWithPayload предполагает акцент на эффективности использования памяти, что важно в коде, критичном к производительности, таком как движок JavaScript.

Анализ причин

Связав всю предысторию связанных функций уязвимости и ссылку на программу POC JavaScript, видно, что проверка SMI (Small Integer) будет выполнена компилятором, и проверка завершится неудачей, поскольку индекс массива a8 установлен как 1073741824, что составляет 2^30. Верхний предел диапазона SMI составляет 2^30 – 1. Однако компилятор Maglev просто вызывает и уменьшает счетчик приоритета и сохраняет уменьшенные результаты для дальнейшей компиляции, что приводит к фатальной ошибке.

После этого мы изучаем исправление /исправление этой ошибки или уязвимости и обнаруживаем, что оно больше не уменьшает счетчик приоритета, а также сохраняем результаты в ValueNode вместо уменьшения результата, чтобы избежать дальнейшей компиляции.

1708004011025.png



Ошибка отправлена - https://bugs.chromium.org/p/chromium/issues/detail?id=1512297, и доказано, что она является дубликатом и будет открыта через 120 дней. К сожалению, мы не можем решить, есть ли какое-либо влияние на безопасность.

Заклчюение: Мы очень благодарны Карлу Смиту из Google V8 Security за тесное сотрудничество и обмен информацией. Желаем приятно провести время в CCC.

Переведено специально для XSS.IS
Автор перевода: yashechka
Источник: https://vxrl.medium.com/root-cause-analysis-of-maglev-graph-builder-vulnerability-482a9b35cdd9
 
Сверху Снизу