РАЗБОР Статья

Fuzzilli против BigSleep: кто ищет баги в V8

Сергей Попов
Сергей Попов Red Team · 6,7 тыс. сообщений
Подписаться
341
[ обложка статьи ]
Режим чтения
Тёмный антистатический коврик с платой отладочного зонда «FUZZILLI» и чёрным алюминиевым модулем «BigSleep» с янтарным светодиодом по бокам от треснувшего кремниевого кристалла V8 с золотыми провод...


По данным Google Project Zero - 45 actionable-уязвимостей за полгода у BigSleep, 37 у Fuzzilli, пересечение 25%. Но стоит поставить BigSleep в контролируемый эксперимент, где он переоткрывает исторические баги Fuzzilli, - пересечение вырастает до 60%. Это не артефакт подсчёта. Два подхода ищут баги в разных пространствах: один перебирает программы, другой строит гипотезы о коде. Русскоязычные публикации по теме ограничились пересказом блог-поста Project Zero. Здесь - разбор методологии по доступным публикациям (конкретные цифры требуют верификации по первоисточникам), баги, которые находит только один подход, и данные по гибридам, пытающимся закрыть gap между coverage-guided fuzzing и AI-driven vulnerability research.

Почему уязвимости движка V8 - критический таргет​

V8 давно перестал быть "движком Chrome". Он стоит за Node.js, Deno, Bun, Electron-приложениями (VS Code, Slack, Discord), Cloudflare Workers, AWS Lambda - от браузера до edge-compute. Одна type confusion в JIT-компиляторе - и вот у тебя RCE через вредоносную веб-страницу и компрометация миллиардов устройств.

Автоматизированный поиск 0-day в V8 - не академическое упражнение, а частный случай более широкой дисциплины, которой посвящено полное руководство по бинарному анализу уязвимостей. Это вопрос operational security для всего стека, который стоит поверх движка.

Scope: почему сравниваются именно Fuzzilli и BigSleep​

1793241297551.webp

Обе системы работают в продакшене Google, обе таргетируют V8, обе репортят через ClusterFuzz в единый баг-трекер Chromium с общей дедупликацией. Единственная пара инструментов с шестимесячными параллельными данными на одном таргете и единой системой подсчёта.

Альтернативные фаззеры JavaScript-движков (Montage, DIE, Comfort) и альтернативные LLM-агенты (CodeMender, ранние версии Naptime) либо не имеют сопоставимого объёма production-статистики, либо работают на других таргетах. StateLens - гибрид с LLM-guided инструментацией - рассматривается отдельно: его данные получены в контролируемом 72-часовом тесте на шести JS-движках, а не в полугодовом продакшене.

Ограничения:
  • Результаты справедливы для V8, Fuzzilli в конфигурации ClusterFuzz и BigSleep с Gemini (версия не подтверждена публично)
  • Cost-per-bug не сравнивается - ни одна система не оптимизирована по стоимости (Project Zero явно это оговаривает)
  • Экстраполяция на SpiderMonkey, JavaScriptCore или другие LLM требует отдельных экспериментов

Архитектура Fuzzilli: фаззинг V8 через промежуточное представление​

Fuzzilli - coverage-guided мутационный фаззер JavaScript-движков, созданный Samuel Gross (Saelo). Принципиальное отличие от grammar-based фаззеров вроде Dharma или Grammarinator: мутации выполняются не на JavaScript-коде, а на промежуточном представлении FuzzIL. Любая FuzzIL-программа гарантированно лифтится в синтаксически и семантически валидный JS. Для фаззинга V8 и JIT-компиляторов, где баги проявляются только на корректном коде, это принципиально.

FuzzIL и REPRL​

FuzzIL-программа - плоский список инструкций, где каждая операция принимает входные переменные и порождает новые:
Код:
v0 <- LoadInteger '0'
v1 <- LoadInteger '10'
v3 <- LoadInteger '0'
BeginFor v0, '<', v1, '+', v2 -> v4
  v6 <- BinaryOperation v3, '+', v4
  Reassign v3, v6
EndFor
v9 <- LoadGlobal 'console'
v10 <- CallMethod v9, 'log', [v3]
Лифтинг тривиален: for (let v4 = 0; v4 < 10; v4++) { v3 = v3 + v4; } console.log(v3); - валидный JavaScript. Каждая мутация имеет малую вероятность сломать семантическую корректность программы, потому что оперирует на уровне переменных и операций, а не синтаксических конструкций.

Выполнение - через REPRL (Read-Eval-Print-Reset-Loop). Модифицированный движок принимает скрипты через pipe и shared memory, выполняет, сбрасывает внутреннее состояние и ждёт следующий скрипт. По измерениям Doyensec, REPRL даёт примерно семикратное ускорение по сравнению с forkserver - экономия на fork() и реинициализации движка. Но за это приходится платить: нужна кастомная модификация JS-движка. Добавление команды fuzzilli с операторами FUZZILLI_CRASH и FUZZILLI_PRINT, настройка дескрипторов для ввода/вывода. Патчи для V8, SpiderMonkey и JavaScriptCore лежат в репозитории Fuzzilli.

Coverage-guided мутации и масштабирование​

Edge coverage собирается через Clang-флаг -fsanitize-coverage=trace-pc-guard. Guard'ы записываются в POSIX shared memory bitmap; после каждого прогона bitmap читается фаззером и guards сбрасываются.

Четыре типа мутаций на FuzzIL:
  • InputMutator - подменяет входные переменные инструкций, перестраивая dataflow программы
  • CodeGenMutator - генерирует новый код или сплайсит фрагменты из корпуса
  • CombineMutator - вставляет целую программу из корпуса в случайную позицию
  • OperationMutator - меняет параметры операций: константы, строки, типы
Масштабирование горизонтальное: инстансы Fuzzilli образуют древовидную иерархию, интересные семплы и крэши поднимаются к parent node, корпус синхронизируется вниз по дереву. Фаззер работает непрерывно - новые коммиты V8 подхватываются в течение нескольких часов.

Что фаззер видит хорошо - нелокальные interaction bugs. Конфликты JIT-оптимизаций, GC и hidden class transitions, где для триггера нужна конкретная комбинация операций. Такое проще нащупать мутацией, чем вывести логически.

Что фаззер не видит - coverage plateau. Когда edge coverage насыщен, баги за разными runtime-состояниями при одинаковом control flow остаются невидимыми. Логические ошибки в спецификации, не открывающие новых edge'ей, - тоже мимо.

BigSleep: LLM-агент Project Zero для поиска уязвимостей V8​

1793241324244.webp

BigSleep - развитие проекта Naptime (2024), LLM-агентная система для автоматизированного поиска уязвимостей в свежем коде. В отличие от Fuzzilli, BigSleep не исследует пространство программ: он читает код, рассуждает о нём и целенаправленно строит тесткейсы.

Агентный цикл: от коммита до PoC​

  1. BigSleep получает список коммитов V8 за сутки
  2. Для каждого diff'а формирует гипотезу: какие инварианты могли нарушиться?
  3. Генерирует PoC-тесткейс на JavaScript
  4. Запускает PoC на pre-fix и post-fix билдах
  5. Крэш на pre-fix, но не на post-fix - файлит через ClusterFuzz
Модель - Gemini (конкретная версия не раскрыта). В экспериментальном режиме - 25 попыток на коммит с разными temperatures и промптами. Это существенно: LLM-вывод недетерминирован, один прогон пропускает значительную часть находимых багов.

Контекст масштаба: по заявлениям Google (точные даты и версии не подтверждены на момент публикации), AI-driven vulnerability detection планируется распространить на всю кодовую базу Chrome. Утверждается, что за несколько milestone количество исправленных security-багов значительно превысило показатели предыдущих релизов. BigSleep в V8 - часть этого заявленного сдвига.

Что агент видит хорошо - логические баги в новом коде, где семантика diff'а указывает на проблему. Spec-level ошибки: некорректная элиминация проверок, truncation, off-by-one в граничных условиях. Агент понимает intent разработчика и проверяет edge cases, не покрытые тестами.

Что агент не видит - нелокальные взаимодействия. Если баг возникает из комбинации двух независимых оптимизаций, commit-review по отдельности видит каждый diff корректным.

Продакшен за полгода: от 441 сырого репорта до 45 уязвимостей​

Данные, предположительно полученные из баг-трекера Chromium (период и цифры не подтверждены независимыми источниками):

МетрикаFuzzilliBigSleep
Сырые баг-репорты316441
После фильтра type:vulnerability17895
Actionable (fix-коммит, без дублей/WAI)3745
Ratio raw -> actionable8.5x9.8x

Переход BigSleep от 441 к 45 - показательная воронка. Значительная часть отсечённых репортов - конфигурационные проблемы (пример: crbug.com/issues/503869331 - некорректная блокировка unsafe-флагов), дубликаты и баги, закрытые как WAI (Working As Intended). Для сравнения фаззера и LLM в контексте security impact они нерелевантны.

Само определение "actionable" тоже стоит покрутить. Fuzzilli-баг crbug.com/issues/511935486: заведён и закрыт ClusterFuzz'ом в один день, ноль developer-взаимодействий. Фактический дубликат crbug.com/issues/511210507, но с другой crash-сигнатурой - ClusterFuzz не распознал совпадение. Project Zero решает проблему жёстким критерием: actionable = не дубликат + не WAI + минимум один fix-коммит, ссылающийся на баг. Критерий воспроизводимый, но заведомо занижает результат обеих систем.

Парадокс пересечения: 25% в продакшене, 60% в эксперименте​

В продакшене 20 из 81 actionable бага найдены обеими системами - 25%. Чуть больше половины из этих 20 первым обнаружил Fuzzilli.

Дедупликация нетривиальна. Project Zero описывает четыре уровня:
  1. Явные дубликаты в баг-трекере (crbug.com/issues/522410841 -> crbug.com/issues/522860268)
  2. "Нерабочие" репорты, привязанные к тому же fix-коммиту через цепочку зависимостей
  3. Дубликаты, отсечённые ещё на уровне ClusterFuzz до попадания в трекер
  4. Формально разные баги, исправленные одним CL (четыре бага в crrev.com/c/7965120 - скорее дубликаты, чем четыре независимые проблемы)
Для оценки теоретического пересечения Project Zero провёл эксперимент. Из исторических багов Fuzzilli (точное количество и период не подтверждены независимо) отобрали 132 воспроизводимых на стандартном debug-билде с минимальным набором флагов, после дедупликации - 112. Для каждого BigSleep получал коммит, предположительно внёсший баг, и делал 25 попыток с Gemini. Критерий успеха: крэш на pre-fix билде, отсутствие крэша на post-fix.

Результат:
  • ~62% багов переоткрыты
  • ~12% коммитов дали другой, уже исправленный баг
  • ~26% - ничего
Дополнительные прогоны (5 попыток + варьирование промптов) добавили ещё 10 багов, итого ~70%. Project Zero оговаривается: аналогичные усилия по тюнингу Fuzzilli тоже могли бы поднять его показатели.

Почему 25% != 60%? Четыре фактора:
  • Окно переоткрытия. В продакшене баг фиксится быстро, вторая система не успевает его найти до патча. В эксперименте BigSleep тестирует pre-fix build - ограничения нет.
  • Разные билды и флаги. Production-конфигурации движка различаются, что влияет на воспроизводимость крэшей.
  • Pipeline outages. Пропущенные коммиты из-за инфраструктурных проблем.
  • Апгрейды модели. За полгода модель BigSleep обновлялась несколько раз; ранние версии, вероятно, находили меньше.
Реальное пересечение способностей двух подходов - ближе к 60%. Но 30-40% багов, которые BigSleep не смог переоткрыть даже в идеальных условиях, - принципиально за пределами LLM на текущем уровне. И неизвестный, но ненулевой процент багов BigSleep находит через semantic reasoning, недоступный coverage-guided exploration.

Отдельное наблюдение: пересечение заметно ниже для JIT-compiler багов, чем для остальных компонентов V8. Гипотеза Project Zero (не тестировалась): баги, которые одновременно нелокальны и требуют сложных тесткейсов, тяжелы для обоих подходов.

Три бага: анатомия различий между фаззером и LLM-агентом​

1793241712412.webp

Конкретные кейсы проясняют архитектурные причины расхождений лучше агрегированной статистики. Project Zero, предположительно, детально разобрал три уязвимости движка V8, каждая из которых показывает сильную сторону одного подхода.

Chrome Bug 449760249: BigSleep находит вариант через patch analysis​

Исходная уязвимость (найденная Fuzzilli двумя годами ранее): некорректное удаление проверки на "дыру" (hole) для переменных в Temporal Dead Zone (TDZ). BigSleep нашёл вариант: labeled break внутри do-while, направленный на внешний switch, не уведомляет loop builder о прерывании. Hole-check элиминируется там, где не должна.

Почему фаззер мимо: coverage feedback не вознаграждает комбинирование labeled break + do-while + switch. Каждая конструкция по отдельности уже покрыта, новых edge'ей при их сочетании нет. Агент действует иначе: читает патч к исходному багу, перечисляет control-flow пути, находит непокрытый и целенаправленно строит PoC. Patch diffing через LLM - и он работает.

Chrome Bug 443765373: усечение handler offset за пределами фаззера​

Handler offset в V8 хранится в 28-битном поле - покрывает 256 МБ байткода. Но байткод может достигать 512 МБ. PoC: функция с ~500 000 операторов yield* -> offset усекается -> переход на невалидный адрес обработчика исключений.

Фаззер технически способен сгенерировать такой тесткейс. На практике - нет: coverage-guided мутации не порождают полумиллионных программ, потому что нет инкрементального сигнала, мотивирующего наращивать размер. Для каждого нового yield* edge coverage не меняется. Агент подходит иначе: видит 28-битное поле, вычисляет предел 2^28, сопоставляет с максимальным размером байткода и строит программу, вызывающую переполнение. Чистый reasoning о числовых границах.

Chrome Bug 40076141: эмерджентный конфликт оптимизаций в Maglev​

Два корректных по отдельности паттерна оптимизации в компиляторе Maglev: MaglevPhiRepresentationSelector решает, что untag можно сделать без проверки (потому что видит Smi-range), а graph builder ранее вставил checked untag, который теперь удаляется. Когда значение выходит за пределы Smi - unchecked path не ловит это, checked убран -> memory corruption.

BigSleep не переоткрыл этот баг ни в одном эксперименте. Причина фундаментальна: баг не локализуется в одном коммите или функции. Он возникает из взаимодействия двух независимых оптимизаций в разных частях компилятора, каждая из которых корректна в изоляции. Commit-review видит каждый diff безопасным. Чтобы найти такое, нужно одновременно подобрать данные за пределами Smi-range и конфигурацию кода, активирующую оба паттерна. Это задача для exploration в пространстве программ, а не для reasoning об одном diff'е.

Меморизация, coverage plateau и путь к гибридам​

LLM помнит баги или обобщает паттерны?​

Закономерный вопрос: если модель обучена на публичных баг-трекерах Chromium, не "вспоминает" ли она уже известные уязвимости?

Project Zero проверил: knowledge cutoff используемой модели Gemini (версия не раскрыта) предшествует периоду тестирования, эффективный cutoff по данным трекера - на несколько месяцев раньше. Для багов до cutoff'а статистически значимой меморизации не обнаружено. Продакшен-результат (45 actionable за полгода) включает исключительно баги в свежих коммитах, которых по определению нет в training data.

Дополнительное наблюдение: баги, на обнаружение которых Fuzzilli тратил больше времени, были слегка сложнее и для BigSleep. Эффект статистически значим, но невелик. Корреляция слабая - подходы не fail в одних и тех же местах. Project Zero рекомендует перепроверять меморизацию с каждой новой моделью: cutoff'ы смещаются, рано или поздно training data будет содержать почти всю историю V8.

StateLens: LLM-guided инструментация как третий путь​

Фундаментальная проблема coverage-guided фаззинга V8 - plateau. Когда edge coverage насыщен, баги скрываются за разными runtime-состояниями при одинаковом control flow: JIT tier transitions, hidden class mutations, GC pressure. Edge coverage к этим различиям слеп.

StateLens (arxiv-идентификатор не подтверждён на момент публикации) предлагает решение через три стадии:
  1. Analysis - LLM-агент на основе knowledge base (комментарии разработчиков, debug-ассерты, баг-репорты) через iterative call-graph и data-flow traversal идентифицирует семантически значимые состояния
  2. Instrumentation - синтезирует read-only probe'ы, проецирующие выбранные состояния в compact shared-memory bitmap
  3. Fuzzing - dual-feedback механизм: фаззер удерживает программы, открывающие новые комбинации состояний, даже при неизменном edge coverage
Результат на шести JS-движках (V8, SpiderMonkey, JavaScriptCore, QuickJS, Hermes, Escargot): 68 новых багов в long-running кампании. В 72-часовом сравнительном тесте StateLens нашёл на 70% больше багов, чем лучший baseline-фаззер.

StateLens принципиально отличается от BigSleep: LLM используется не для PoC-генерации, а для инструментации. Exploration остаётся за мутационным движком. Semantic understanding от LLM определяет где искать, coverage-guided engine - как искать. Именно эта гибридная архитектура может закрыть gap между двумя "чистыми" парадигмами.

Сравнение фаззера и LLM-агента: решающие критерии​

1793241644946.webp

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме


Когда запускать фаззер: таргет - JIT-компилятор (Turbofan, Maglev), баги из нелокальных взаимодействий. Есть compute для непрерывного прогона на десятках инстансов. Нужно покрытие каждого коммита в течение часов. Ищете memory corruption: type confusion, OOB, UAF.

Когда запускать LLM-агент: фокус на новых language features, parser changes, spec updates. Баг-классы логические - некорректная элиминация проверок, truncation, граничные условия. Daily commit-review pipeline достаточен. Готовы к triage overhead (~10 сырых репортов на 1 actionable).

Когда нужны оба: всегда, если ресурсы позволяют. При 25%-ном пересечении в продакшене отказ от одного подхода оставляет 30-40% actionable-уязвимостей без покрытия. Project Zero формулирует прямо: "vendors perform both fuzzing and agentic analysis of their code to maximize the number of vulnerabilities that get reported before they impact users""Разработчики проводят как фаззинг, так и агентный анализ своего кода, чтобы выявить максимальное число уязвимостей до того, как они затронут пользователей.".

Все обсуждают headline - 45 против 37, "ничья с лёгким перевесом BigSleep". На деле главная цифра в отчёте Project Zero - не эта. Главная цифра - 30-40% багов, недоступных BigSleep даже при 25 попытках на коммит в идеальных условиях, плюс зеркальная прослойка багов, которые LLM находит, а фаззер - нет. Парадигма "один инструмент закрывает V8" мертва. Один инструмент покрывает 60-70% поверхности. Треть уязвимостей остаётся.

Через пару лет чистый coverage-guided фаззинг без LLM-инструментации состояний будет выглядеть так же, как blackbox-фаззинг выглядит сейчас - работающий, но архаичный. Данные StateLens (70% прирост) показывают вектор: LLM не заменяет фаззер, а переводит его в пространство состояний, где edge coverage слеп. Но и чистые LLM-агенты упёрлись в потолок нелокальных взаимодействий, где reasoning об одном коммите недостаточен. Ближайшее будущее автоматизированного поиска 0-day - за трёхслойной архитектурой: LLM-analysis для инструментации + coverage-guided exploration для interaction bugs + commit-review agent для логических ошибок. Кто из вендоров первым соберёт этот pipeline в production - получит год-два форы. Если хочется понять, как type confusion и OOB эксплуатируются в живом движке - pwn-категория на HackerLab.pro (ХакерЛаб) даёт именно это, без подсказок.
Полезно

Комментарии

0