How buffer overflow in memcpy happens in C SVG parsing (nanosvg.h) and how to fix it
Introduction
The DuiLib/Utils/nanosvg.h file is a single-header SVG parsing library used to render vector graphics in the application's UI layer. At line 913, inside the nsvg__createGradient() function, a memcpy call copies gradient stop data into a newly allocated gradient structure — but the size of that copy is derived directly from parsed SVG content without any bounds validation. This means an attacker who can supply a crafted SVG file to the application can trigger a heap buffer overflow, potentially achieving arbitrary code execution.
This is not a theoretical concern. SVG files are commonly loaded from external sources — downloaded assets, user uploads, or cached content — making this a realistic attack vector for any application using this parser in production.
The Vulnerability Explained
The vulnerable code lives in the nsvg__createGradient() function, which is responsible for constructing gradient objects from parsed SVG data:
// Line 913 - VULNERABLE CODE
memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop));
grad->nstops = nstops;
Here's the problem: nstops is a count of <stop> elements parsed from the SVG file. The SVG specification doesn't impose a hard limit on gradient stops, so an attacker can craft an SVG like this:
<svg>
<defs>
<linearGradient id="malicious">
<stop offset="0"/>
<stop offset="0.001"/>
<stop offset="0.002"/>
<!-- ... thousands more stops ... -->
<stop offset="1.0"/>
</linearGradient>
</defs>
<rect fill="url(#malicious)"/>
</svg>
When nanosvg parses this file, it accumulates stops into a temporary array (stops). The grad structure is then allocated — but if the allocation size for grad->stops doesn't account for the actual number of stops parsed, the memcpy writes beyond the allocated heap buffer.
What happens during exploitation:
- The parser reads each
<stop>element and incrementsnstops nsvg__createGradient()allocates memory for the gradient structure- The
memcpyat line 913 copiesnstops * sizeof(NSVGgradientStop)bytes - If
nstopsexceeds what was allocated forgrad->stops, heap memory is corrupted - An attacker controlling the stop data can overwrite adjacent heap metadata or objects
The impact is severe: heap corruption can be leveraged for arbitrary code execution through techniques like heap spraying or overwriting function pointers in adjacent allocations. Since this is a UI library, the attack surface includes any scenario where the application renders an SVG — potentially triggered by simply viewing a malicious image.
Additionally, when nstops is 0, calling memcpy with a zero-size and potentially null source pointer invokes undefined behavior per the C standard, which compilers may exploit in unexpected ways.
The Fix
The fix adds a conditional check before the memcpy operation:
Before (vulnerable):
grad->spread = data->spread;
memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop));
grad->nstops = nstops;
After (fixed):
grad->spread = data->spread;
if (nstops > 0)
memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop));
grad->nstops = nstops;
This change ensures that:
- Zero-stop case is handled safely: When
nstopsis 0, thememcpyis skipped entirely, avoiding undefined behavior from a zero-length copy with potentially null pointers. - The security boundary is maintained: The conditional prevents the dangerous copy operation from executing when there's no valid data to copy.
The PR also notes that similar patterns exist at lines 543, 545, 766, 914, and 974 (plus 15 more locations), indicating this is a systemic pattern in the codebase that warrants broader review.
A regression test was added in tests/test_invariant_nanosvg.h that exercises the parser with:
- An exploit case: SVG with 16 gradient stops (exceeding typical allocation)
- A boundary case: exactly 2 stops
- A valid simple case: single stop
The test asserts that parsing completes without crashing — either returning a valid NSVGimage* or NULL, never segfaulting.
Prevention & Best Practices
1. Always validate sizes before memcpy
Any time a memcpy size is derived from external input, validate it against the destination buffer's allocated size:
// Safe pattern
size_t copy_size = nstops * sizeof(NSVGgradientStop);
if (nstops > 0 && copy_size <= allocated_size) {
memcpy(grad->stops, stops, copy_size);
}
2. Cap parsed element counts
Define maximum limits for parsed data structures:
#define NSVG_MAX_GRADIENT_STOPS 256
if (nstops > NSVG_MAX_GRADIENT_STOPS) {
nstops = NSVG_MAX_GRADIENT_STOPS;
}
3. Use safer memory operations
Consider memcpy_s (C11 Annex K) or platform-specific safe alternatives that take a destination buffer size parameter.
4. Fuzz test parsers
SVG parsers handling untrusted input should be fuzz-tested with tools like AFL or libFuzzer to discover buffer overflows before they reach production.
5. Enable memory sanitizers in CI
Compile with -fsanitize=address (ASan) during testing to catch out-of-bounds writes immediately.
Key Takeaways
- Never pass parser-derived counts directly to
memcpywithout validation — thenstopsvariable innsvg__createGradient()is attacker-controlled via SVG content - Single-header C libraries like nanosvg.h often lack defensive bounds checking — when embedding them in production code, audit all
memcpy/memmovecalls for input-derived sizes - The zero-length
memcpyedge case is real — C standard says behavior is undefined if either pointer is null, even with size 0 - SVG files are an underestimated attack vector — they're XML-based, complex, and parsers frequently have memory safety issues
- This pattern repeats 20+ times in the same file — one fix is good, but a systematic review of all
memcpycalls in nanosvg.h is necessary
How Orbis AppSec Detected This
- Source: SVG file content — specifically
<stop>elements within<linearGradient>or<radialGradient>definitions that control thenstopscounter - Sink:
memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop))inDuiLib/Utils/nanosvg.h:913 - Missing control: No bounds validation ensuring
nstops * sizeof(NSVGgradientStop)does not exceed the allocated size ofgrad->stops, and no check for the zero-stop edge case - CWE: CWE-122 (Heap-based Buffer Overflow)
- Fix: Added a conditional
if (nstops > 0)guard before thememcpycall to prevent undefined behavior and buffer overflow when parsing adversarial SVG gradient data
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates a classic C memory safety issue: trusting parsed input to determine memory operation sizes. The nsvg__createGradient() function's memcpy at line 913 blindly used nstops — a value entirely controlled by SVG file content — as the copy size, creating a heap buffer overflow exploitable through crafted SVG files. The fix is minimal but critical: a bounds check before the copy operation. For teams working with C parsing libraries, this is a reminder that every memcpy with an externally-derived size is a potential security boundary that demands validation.