Back to Blog
critical SEVERITY9 min read

How heap buffer overflow happens in C tmap.c memcpy and how to fix it

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.

O
By Orbis AppSec
Published June 18, 2026Reviewed June 18, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in C, specifically in the `jsdrv_tmap_copy()` function in `src/tmap.c`. The root cause is that `memcpy` operations used computed sizes derived from `src->tail` and `src->head` without first validating those values against the allocated buffer capacity (`src->alloc_size`). An attacker who can send crafted time map data over USB could trigger out-of-bounds writes, causing heap corruption or arbitrary code execution. The fix adds an explicit bounds check—`if ((src->head > src->alloc_size) || (src->tail >= src->alloc_size))`—before the `tmap_size()` computation, returning NULL early on invalid input.

Vulnerability at a Glance

cweCWE-122
fixAdded bounds check before tmap_size() call in jsdrv_tmap_copy() to return NULL on invalid indices
riskHeap corruption or arbitrary code execution via crafted USB device data
languageC
root causesrc->head and src->tail used in size computation without validation against src->alloc_size
vulnerabilityHeap Buffer Overflow via unchecked memcpy

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->tail is set to a value near 0xFFFF
  • src->head is set beyond src->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 runtime memcpy bounds 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->tail and src->head are 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 memcpy calls were all protected by one guard: Placing the bounds check before tmap_size() is called means all downstream memory operations in jsdrv_tmap_copy() inherit the protection—no need to add checks at each individual memcpy.
  • The >= vs > distinction matters: tail >= alloc_size is the correct check for an index (must be strictly less than size), while head > alloc_size allows 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 0xAA canary in test_tmap_bounds_safety provides 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->tail and src->head in a jsdrv_tmap_s structure
  • Sink: memcpy operations at src/tmap.c:111, src/tmap.c:114, src/tmap.c:117, and src/tmap.c:119, where sizes are computed from the unvalidated src->tail and src->head fields via tmap_size(src)
  • Missing control: No bounds check validating src->head and src->tail against src->alloc_size before the size computation in jsdrv_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 of src/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.


References

Frequently Asked Questions

What is a heap buffer overflow in C memcpy?

A heap buffer overflow occurs when a memcpy writes more bytes than the destination buffer on the heap can hold, because the size argument was computed from unvalidated input. This can corrupt adjacent heap metadata or data, potentially enabling arbitrary code execution.

How do you prevent heap buffer overflow in C memcpy calls?

Always validate that source indices and computed sizes are within the bounds of the allocated buffer before calling memcpy. Use explicit checks like `if (index >= alloc_size) return NULL;` before computing sizes derived from those indices.

What CWE is heap buffer overflow?

Heap buffer overflow is classified as CWE-122 (Heap-based Buffer Overflow), a subset of CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is AddressSanitizer enough to prevent heap buffer overflow in production?

AddressSanitizer (ASan) is an excellent detection tool during testing, but it is not a production mitigation. You must fix the root cause—adding explicit bounds checks—so that invalid inputs are rejected before any memory operation occurs.

Can static analysis detect unchecked memcpy bounds in C?

Yes. Static analysis tools like Semgrep, Coverity, and clang-analyzer can flag memcpy calls where the size argument is derived from user-controlled or externally-sourced values without prior validation. Orbis AppSec's multi-agent AI scanner detected exactly this pattern in tmap.c.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #22

Related Articles

critical

How buffer overflow happens in C++ and how to fix it

A critical buffer overflow in `create_hex_string()` within `hmlangw.cpp` let an unconditional 16-iteration loop write past the bounds of a 100-byte `hex` buffer using unchecked `sprintf` calls. The fix replaces `sprintf` with `snprintf` and caps the loop iterations based on the actual destination buffer size, closing off a memory corruption path reachable from serial or network input.

high

How insecure-use-string-copy-fn happens in C and how to fix it

A high-severity vulnerability was identified in `plugin/bin/install.c` where `strcpy()` and `strncpy()` were used to handle path strings without proper bounds checking or guaranteed null-termination. The fix replaces `strcpy()` with direct character assignment and `strncpy()` with `snprintf()`, eliminating both buffer overflow and missing null-terminator risks in the plugin installation workflow.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

high

How insecure string copy functions happen in C and how to fix it

A high-severity buffer overflow vulnerability was discovered in `tools/claude-vscode-wrapper.c`, where an unbounded `strcpy()` call copied a file path into a fixed-size `MAX_PATH` buffer without any size validation. The fix replaces `strcpy()` with `snprintf()` and swaps `malloc()` for `calloc()`, ensuring both string operations and memory allocation are bounds-safe and zero-initialized.

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.