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 strcpy buffer overflow happens in C++ debugger command handling and how to fix it

A critical stack-based buffer overflow was discovered in `src/debugger.cpp` at line 387, where `strcpy` copied user-entered debugger commands into a fixed-size stack buffer (`prevCommandBuffer`) without any length validation. An attacker could craft an oversized command string to overflow the buffer, overwrite the return address, and achieve arbitrary code execution. The fix replaces `strcpy` with bounded `strncpy` and explicit null-termination.

critical

How out-of-bounds reads happen in C gettext .mo file parsers and how to fix it

A missing bounds check in the gettext `.mo` file parser inside `compose/asc-utils-l10n.c` allowed a malformed or truncated file to trigger out-of-bounds reads from heap memory. The vulnerability affected two distinct read sites — a `memcpy` of the full `AscLocaleGettextHeader` struct at line 131 and a 4-byte offset read at line 224 — neither of which validated that the source buffer was large enough. The fix adds explicit size checks before both reads, rejecting invalid files with a descriptive

critical

How buffer overflow in SMS response buffer handling happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `sm_at_sms.c`, where three consecutive unsafe string operations — `sprintf()`, `strcpy()`, and `strcat()` — wrote SMS payload data into a fixed-size buffer without any bounds checking. An attacker capable of crafting an oversized SMS message could overflow `sms_ctx.concat_rsp_buf`, corrupting adjacent stack or heap memory. The fix replaces all three unsafe calls with their bounds-aware counterparts: `snprintf()` and `strcat_s()`.

critical

How integer overflow in regexJIT.c heap allocation happens in C and how to fix it

A critical integer overflow vulnerability in `regex_src/regexJIT.c` allowed crafted regex patterns to trigger a heap buffer overflow by causing an unchecked multiplication of `sizeof(struct stack_item) * dfa_size` to wrap around on 32-bit platforms, resulting in an undersized allocation. The fix adds a pre-allocation overflow guard that returns `REGEX_MEMORY_ERROR` before any dangerous write can occur. Left unpatched, this vulnerability could be exploited to corrupt heap memory, crash the proces

critical

How kernel stack buffer overflow happens in C vsprintf() and how to fix it

A critical stack buffer overflow vulnerability was discovered in `sys/kern/debug.c` where the kernel's `printf()` function called a custom `vsprintf()` implementation without any length constraint on the output buffer `db_msg`. By replacing the unbounded `vsprintf()` call with a size-aware `vsnprintf()` implementation, the fix prevents crafted format strings or oversized arguments from overwriting kernel stack memory, closing a path to arbitrary kernel code execution.

critical

How command injection happens in Python subprocess and how to fix it

A command injection vulnerability in `skills/skill-comply/scripts/runner.py` allowed attackers who could influence skill definition files to execute arbitrary binaries on the host system via `subprocess.run()`. The fix introduces an explicit allowlist of permitted executables (`ALLOWED_SETUP_EXECUTABLES`) that gates every command before it reaches the subprocess call at line 110. This closes a significant attack surface in the skill-comply pipeline without breaking legitimate setup workflows.