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

high

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

A high-severity buffer overflow vulnerability was discovered in `src/calculations.c` at line 37, where a two-step `strncpy` + manual null-termination pattern left the door open for subtle memory safety bugs when copying string data into the `entry->type` field. The fix replaces both lines with a single `snprintf` call that handles bounds and null-termination atomically, eliminating the risk entirely. This is a common C pitfall that affects production CLI tools and can be exploited when attacker-

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How integer overflow in buffer size calculation happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).

critical

How integer overflow happens in C reliable.c and how to fix it

A critical integer overflow vulnerability was discovered in `reliable.c` at line 1299, where the `packet_buffer_size` calculation used signed `int` arithmetic that could wrap to a negative or undersized value when large `fragment_size` values were involved. By casting each operand to `size_t` before multiplication, the fix eliminates the overflow risk entirely and ensures the allocated buffer is always large enough to hold the reassembled packet data.

high

How insecure string copy functions happen in C (cyw43.c) and how to fix it

Three unsafe string copy calls in `src/cyw43.c` — including a bare `strcpy()` and two `strncpy()` calls — created buffer overflow risks in a CYW43 Wi-Fi driver emulation layer. The fix replaces all three with `snprintf()`, which enforces buffer size limits and guarantees null-termination in a single, consistent operation. Left unaddressed, these vulnerabilities could allow an attacker controlling input like a TAP interface name or SSID to corrupt adjacent memory and potentially execute arbitrary