How heap buffer overflow happens in C tmap.c memcpy and how to fix it
Summary
A missing bounds validation in jsdrv_tmap_copy() within src/tmap.c allowed crafted time map data—delivered over USB device communication—to trigger a heap buffer overflow via unchecked memcpy operations. The fix adds a three-line guard that validates src->head and src->tail against src->alloc_size before any memory is copied, closing a confirmed-exploitable attack path in production code.
Introduction
The src/tmap.c file handles time map processing for USB device communication in the jsdrv library. Its job is to maintain a circular buffer of time-to-counter mapping entries that synchronize host timestamps with device sample counters. When jsdrv_tmap_copy() is called to duplicate one of these maps, it computes a copy size using tmap_size(src)—a value derived directly from src->tail and src->head. The problem: before this PR, neither field was checked against src->alloc_size.
If a connected USB device (or a piece of software impersonating one) sends crafted time map data with src->tail or src->head set to values exceeding the allocated buffer, the subsequent memcpy operations at lines 111, 114, 117, and 119 will write past the end of the destination heap allocation. The result is heap corruption—and on systems where heap metadata is adjacent to the buffer, potentially arbitrary code execution.
This is not a theoretical concern. The PR's assessment is confirmed exploitable, and the scanner flagged multiple lines in the same function using the same unsafe pattern.
The Vulnerability Explained
The Dangerous Pattern
In jsdrv_tmap_copy(), the function receives a const struct jsdrv_tmap_s * src pointer. Before the fix, the code immediately computed a size from the source structure's internal fields:
// VULNERABLE — before the fix (src/tmap.c ~line 102)
struct jsdrv_tmap_s * jsdrv_tmap_copy(const struct jsdrv_tmap_s * src) {
if (NULL == src) {
return NULL;
}
// No validation of src->head or src->tail here!
size_t sz = tmap_size(src); // <-- sz computed from untrusted fields
struct jsdrv_tmap_s * self = jsdrv_tmap_alloc(sz);
if (sz == 0) {
// ...
}
// memcpy operations at lines 111, 114, 117, 119 use sz or derived counts
// derived from src->tail and head_count — all unvalidated
The tmap_size() helper computes the number of entries to copy based on src->tail (the circular buffer's tail index) and related fields. If src->tail is, say, 0xFFFF but src->alloc_size is only 64, the computed sz will be enormous—and the memcpy at line 111 will happily write that many bytes onto the heap, far beyond the destination buffer's capacity.
The PR notes that four lines in this function share the same vulnerable pattern: lines 111, 114, 117, and 119. These correspond to the two-part copy of a wrapped circular buffer (head segment, then tail segment), repeated across copy and merge operations.
The Attack Scenario
An attacker with the ability to send USB device communication to a host running jsdrv can craft a time map packet where:
src->tailis set to a value near0xFFFFsrc->headis set beyondsrc->alloc_size
When the host processes this packet and calls jsdrv_tmap_copy(), the unvalidated fields flow directly into tmap_size(), producing an inflated sz. The subsequent memcpy at line 111:
memcpy(dst, &entries[tail], head_count * sizeof(struct jsdrv_time_map_s));
...writes head_count * sizeof(struct jsdrv_time_map_s) bytes—potentially hundreds of kilobytes—past the end of dst, corrupting the heap. On a system with a predictable heap layout, this is a reliable primitive for code execution.
Real-World Impact
This library handles USB device communication, meaning the attack surface includes any physical USB device connected to a host, or any software that can inject data into the jsdrv communication pipeline. Heap corruption of this kind can lead to:
- Process crashes (denial of service)
- Heap metadata corruption enabling further exploitation
- Arbitrary code execution in the context of the process using jsdrv
The Fix
What Changed
The fix is surgical and correct: three lines added to jsdrv_tmap_copy() immediately after the NULL check, before any size computation occurs:
// FIXED — src/tmap.c
struct jsdrv_tmap_s * jsdrv_tmap_copy(const struct jsdrv_tmap_s * src) {
if (NULL == src) {
return NULL;
}
// NEW: validate indices before using them to compute sizes
if ((src->head > src->alloc_size) || (src->tail >= src->alloc_size)) {
return NULL;
}
size_t sz = tmap_size(src);
struct jsdrv_tmap_s * self = jsdrv_tmap_alloc(sz);
// ...
Before vs. After
| Before | After | |
|---|---|---|
src->head validation |
None | src->head > src->alloc_size → return NULL |
src->tail validation |
None | src->tail >= src->alloc_size → return NULL |
tmap_size() call |
Unconditional | Only reached with validated indices |
memcpy operations |
Unchecked sizes | Sizes derived from validated fields |
Notice the subtle but intentional asymmetry: head uses > (allowing head == alloc_size as a valid "full buffer" sentinel) while tail uses >= (tail must be a valid index, strictly less than alloc_size). This reflects the semantics of the circular buffer design.
Why This Specific Check Is Sufficient
Once src->head and src->tail are proven to be within [0, alloc_size], the tmap_size() computation is bounded. The circular buffer arithmetic—splitting the copy into a head segment (&entries[tail] to end of array) and a tail segment (&entries[0] to wrap point)—cannot produce a total count exceeding alloc_size. The downstream memcpy calls at lines 111, 114, 117, and 119 are all protected by this single early-exit guard.
The Regression Test
A new test file test/test_invariant_tmap.c was added with test_tmap_bounds_safety, which exercises four adversarial (tail, length) combinations:
struct { uint16_t tail; uint16_t length; } cases[] = {
{JSDRV_TIME_MAP_LENGTH - 1, JSDRV_TIME_MAP_LENGTH}, // tail near end, full wrap
{0, JSDRV_TIME_MAP_LENGTH}, // no wrap, full capacity
{JSDRV_TIME_MAP_LENGTH / 2, JSDRV_TIME_MAP_LENGTH}, // mid wrap, full capacity
{0, 1}, // minimal valid input
};
Each case places a 16-byte guard array (0xAA-filled) adjacent to the destination buffer and asserts it is unmodified after the copy. This is a canary-style test that will catch any regression where a bounds check is removed or weakened.
Prevention & Best Practices
1. Validate Before You Compute
The root cause here is that src->tail and src->head were used as inputs to a size computation before being validated. The general rule: any value that crosses a trust boundary (network, USB, IPC) must be validated against known-good bounds before it is used arithmetically.
// Pattern to follow for circular buffer operations in C:
if (index >= buffer->alloc_size) {
return ERROR_INVALID_ARGUMENT;
}
size_t safe_count = compute_count(index, buffer->alloc_size); // now safe
memcpy(dst, &buffer->entries[index], safe_count * sizeof(*dst));
2. Use Safe Memory Operation Wrappers
Consider wrapping memcpy calls in a helper that enforces destination capacity:
static inline int safe_memcpy(void *dst, size_t dst_cap,
const void *src, size_t n) {
if (n > dst_cap) return -1;
memcpy(dst, src, n);
return 0;
}
3. Enable Compiler and Sanitizer Hardening
-D_FORTIFY_SOURCE=2: Enables glibc's compile-time and runtimememcpybounds checking for statically-known buffer sizes.- AddressSanitizer (
-fsanitize=address): Detects out-of-bounds heap writes at runtime during testing. -fstack-protector-strong: Adds stack canaries (less relevant for heap overflows, but good practice).- Valgrind/Helgrind: Useful for detecting heap corruption in integration tests.
4. Fuzz the USB Input Path
Since the attack surface here is USB device communication, the time map parsing code is an excellent target for fuzzing with a tool like libFuzzer or AFL++. A fuzzer would have found this quickly by generating random tail/head values.
5. Relevant Standards
- CWE-122: Heap-based Buffer Overflow
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow Prevention Cheat Sheet
- SEI CERT C: Rule ARR38-C — Guarantee that library functions do not form invalid pointers
Key Takeaways
src->tailandsrc->headare attacker-controlled via USB: Any field sourced from device communication must be treated as untrusted input and range-checked before arithmetic use.tmap_size()was only safe with validated inputs: The helper function itself was not the problem—the problem was calling it with fields that hadn't been validated yet.- Four
memcpycalls were all protected by one guard: Placing the bounds check beforetmap_size()is called means all downstream memory operations injsdrv_tmap_copy()inherit the protection—no need to add checks at each individualmemcpy. - The
>=vs>distinction matters:tail >= alloc_sizeis the correct check for an index (must be strictly less than size), whilehead > alloc_sizeallows the "full buffer" sentinel value. Getting this wrong would either allow off-by-one overflows or incorrectly reject valid full-buffer copies. - Regression tests with guard bytes are the right pattern: The
0xAAcanary intest_tmap_bounds_safetyprovides a concrete, runtime-verifiable invariant that bounds are respected—not just a logical assertion.
How Orbis AppSec Detected This
- Source: Crafted time map data received via USB device communication, populating
src->tailandsrc->headin ajsdrv_tmap_sstructure - Sink:
memcpyoperations atsrc/tmap.c:111,src/tmap.c:114,src/tmap.c:117, andsrc/tmap.c:119, where sizes are computed from the unvalidatedsrc->tailandsrc->headfields viatmap_size(src) - Missing control: No bounds check validating
src->headandsrc->tailagainstsrc->alloc_sizebefore the size computation injsdrv_tmap_copy() - CWE: CWE-122 — Heap-based Buffer Overflow
- Fix: Added
if ((src->head > src->alloc_size) || (src->tail >= src->alloc_size)) { return NULL; }at line 105 ofsrc/tmap.c, before any size computation or memory operation
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
The jsdrv_tmap_copy() heap buffer overflow is a textbook example of how circular buffer operations in C become dangerous the moment their index fields are sourced from an external device without validation. The vulnerability wasn't in memcpy itself—it was in the implicit assumption that src->tail and src->head would always be sane. A three-line bounds check before tmap_size() is called eliminates the entire class of overflow for this function.
For developers working with device communication protocols in C: treat every field in a received data structure as adversarial until proven otherwise. Validate indices against allocation sizes before arithmetic. Fuzz your parsers. And use guard-byte regression tests to make "buffer overflow is impossible here" a machine-checked invariant, not just a code review assertion.