Back to Blog
critical SEVERITY6 min read

How buffer overflow in memcpy() happens in C/C++ embedded firmware and how to fix it

A critical buffer overflow vulnerability was discovered in the ESP32-based micro-journal firmware where `memcpy()` calls used `strlen()` without bounds checking, allowing oversized USB descriptor strings to corrupt adjacent memory. The fix replaces unbounded `strlen()` with `strnlen()` calls that enforce the destination buffer sizes (8, 16, and 4 bytes respectively), preventing heap/stack corruption from malicious USB devices.

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

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C/C++ embedded firmware (ESP32) where `memcpy()` copies USB descriptor strings using `strlen()` as the size parameter without validating the source fits in fixed-size destination buffers. The fix replaces `strlen(vid)` with `strnlen(vid, 8)`, `strlen(pid)` with `strnlen(pid, 16)`, and `strlen(rev)` with `strnlen(rev, 4)` to enforce maximum copy lengths matching the destination buffer sizes.

Vulnerability at a Glance

cweCWE-120
fixReplace strlen() with strnlen() bounded to destination buffer size
riskMemory corruption, potential code execution via malicious USB device
languageC/C++ (ESP32 embedded firmware)
root causeUsing strlen() as memcpy size without validating it fits the fixed-size destination buffer
vulnerabilityBuffer overflow via unchecked memcpy()

How buffer overflow in memcpy() happens in C/C++ embedded firmware and how to fix it

Introduction

In the micro-journal ESP32 firmware repository, we discovered a critical buffer overflow in micro-journal-rev-4-esp32/lib/FatFSUSB/FatFSUSB.cpp at line 176. The tud_msc_inquiry_cb() callback function—responsible for responding to USB Mass Storage Class INQUIRY commands—copies vendor, product, and revision strings into fixed-size buffers using memcpy() with strlen() as the size parameter, without any validation that the source string actually fits within the destination.

While the current hardcoded strings ("ESP32S3", "Mass Storage", "1.0") happen to fit their respective buffers, this code pattern is inherently unsafe. If these strings are ever changed, derived from configuration, or if the callback receives externally-influenced data, the result is immediate memory corruption. More critically, in a USB host scenario, a malicious device presenting oversized descriptor strings could exploit this exact pattern to overflow the buffers and corrupt adjacent memory on the ESP32.

The Vulnerability Explained

The vulnerability exists in the tud_msc_inquiry_cb function, which is a TinyUSB callback invoked when a USB host (or device in host mode) sends an INQUIRY command. The function signature provides three fixed-size output buffers:

extern "C" void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4])

The buffer sizes are defined by the SCSI specification:
- vendor_id: 8 bytes
- product_id: 16 bytes
- product_rev: 4 bytes

The vulnerable code at lines 176-178 was:

const char vid[] = "ESP32S3";
const char pid[] = "Mass Storage";
const char rev[] = "1.0";

memcpy(vendor_id, vid, strlen(vid));
memcpy(product_id, pid, strlen(pid));
memcpy(product_rev, rev, strlen(rev));

Why is this dangerous?

The strlen() function returns the length of the source string with zero awareness of the destination buffer's capacity. This creates a classic CWE-120 condition where the copy operation is bounded only by the source, not the destination.

Concrete attack scenario: Consider a firmware modification or configuration change where vid becomes a longer string—say "Espressif Systems" (17 characters). The memcpy(vendor_id, vid, strlen(vid)) call would write 17 bytes into an 8-byte buffer, overwriting 9 bytes of whatever sits adjacent in memory. On an ESP32, this could corrupt:

  • The stack frame's return address (enabling code execution)
  • Adjacent local variables (altering program logic)
  • The product_id or product_rev buffers themselves (since they're likely contiguous in the stack frame)

Physical attack vector: An attacker with physical access could connect a malicious USB device that triggers this callback path. If the descriptor strings are ever sourced from the connected device's data (a common pattern in USB host implementations), oversized strings would directly overflow these buffers.

The PR notes also identify similar vulnerable patterns at lines 133, 249, and 259 in the same file, suggesting this is a systemic coding pattern rather than an isolated mistake.

The Fix

The fix is elegant and minimal—replacing unbounded strlen() with strnlen() that enforces the destination buffer sizes:

Before (vulnerable):

memcpy(vendor_id, vid, strlen(vid));
memcpy(product_id, pid, strlen(pid));
memcpy(product_rev, rev, strlen(rev));

After (fixed):

memcpy(vendor_id, vid, strnlen(vid, 8));
memcpy(product_id, pid, strnlen(pid, 16));
memcpy(product_rev, rev, strnlen(rev, 4));

How strnlen() solves the problem

The strnlen(s, maxlen) function returns the length of string s, but never more than maxlen. This means:

  • strnlen(vid, 8) → returns min(strlen(vid), 8) → copy is capped at 8 bytes
  • strnlen(pid, 16) → returns min(strlen(pid), 16) → copy is capped at 16 bytes
  • strnlen(rev, 4) → returns min(strlen(rev), 4) → copy is capped at 4 bytes

No matter how long the source strings become—whether through code changes, configuration, or external input—the copy operation can never write beyond the destination buffer's boundaries. The SCSI INQUIRY response buffers are space-padded by convention, so truncation is acceptable and expected behavior.

Regression test

The PR also includes a comprehensive Google Test suite (tests/test_invariant_FatFSUSB.cpp) that validates the security invariant with adversarial inputs:

INSTANTIATE_TEST_SUITE_P(
    AdversarialInputs,
    UsbDescriptorSecurityTest,
    ::testing::Values(
        std::string(256, 'A'),   // Far exceeds all buffers
        std::string(17, 'B'),    // One byte over product_id (16)
        std::string(8, 'C'),     // Exactly at vendor_id limit
        std::string("VID")       // Valid: well within limits
    )
);

This test uses canary values around the buffers to detect any overflow, ensuring the fix holds under boundary conditions.

Prevention & Best Practices

  1. Never use strlen() as the sole size parameter for memcpy() when copying into fixed-size buffers. Always use strnlen() or compare against the destination size first.

  2. Prefer strncpy() or snprintf() for string copies where null-termination is needed. For SCSI descriptor buffers (which are space-padded, not null-terminated), memcpy with strnlen is appropriate.

  3. Use compiler warnings: GCC's -Wstringop-overflow and Clang's -Wbuffer-overflow can catch some of these patterns at compile time.

  4. Static analysis in CI: Tools like Coverity, PVS-Studio, or even Semgrep with custom rules can flag memcpy(dst, src, strlen(src)) patterns where dst has a known fixed size.

  5. Embedded-specific considerations: On ESP32 and similar microcontrollers, ASLR and stack canaries may not be available, making buffer overflows even more reliably exploitable.

  6. Code review checklist: Any memcpy call should have a comment or assertion documenting that the size parameter is bounded by the destination capacity.

Key Takeaways

  • memcpy(buf, str, strlen(str)) into a fixed-size buffer is always a latent vulnerability—even when current string values fit, future changes or external input can trigger overflow.
  • The tud_msc_inquiry_cb callback's buffer sizes (8, 16, 4) are defined by the SCSI spec, not by the firmware—so the firmware must enforce these limits regardless of source string content.
  • strnlen(s, max) is the minimal-change fix that preserves the existing code structure while adding bounds enforcement.
  • Physical attack vectors on embedded devices are real—a malicious USB device with oversized descriptors could exploit this pattern if the firmware ever processes device-supplied strings through this path.
  • Similar patterns at lines 133, 249, and 259 in the same file indicate systematic review is needed when one instance of an unsafe pattern is found.

How Orbis AppSec Detected This

  • Source: USB descriptor string data (the vid, pid, and rev character arrays) that could be influenced by configuration changes or external USB device data
  • Sink: memcpy(vendor_id, vid, strlen(vid)) at FatFSUSB.cpp:176 where the copy size is unbounded relative to the 8-byte destination buffer
  • Missing control: No validation that strlen() of the source string is less than or equal to the destination buffer's fixed capacity (8, 16, or 4 bytes)
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Replaced strlen() with strnlen() bounded to each destination buffer's size (8, 16, and 4 bytes respectively)

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 buffer overflow in FatFSUSB.cpp demonstrates how a seemingly harmless code pattern—memcpy with strlen—creates a critical vulnerability in embedded firmware. The fix is straightforward: use strnlen() to enforce destination buffer limits. For embedded developers working with USB stacks, SCSI callbacks, or any fixed-size protocol buffers, always ensure your copy operations are bounded by the destination size, not the source size. Memory safety in embedded systems requires extra vigilance because the hardware-level protections available on desktop systems (ASLR, stack canaries, guard pages) are often absent.

References

Frequently Asked Questions

What is a buffer overflow via memcpy?

A buffer overflow via memcpy occurs when the number of bytes copied exceeds the destination buffer's allocated size, writing data into adjacent memory and potentially corrupting program state or enabling code execution.

How do you prevent buffer overflow in C/C++ memcpy calls?

Use bounded copy functions like strnlen() to cap the copy length to the destination buffer size, or use safer alternatives like strncpy() or snprintf() that inherently respect buffer boundaries.

What CWE is buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers classic buffer overflows where data is copied to a buffer without verifying the source data fits within the destination's allocated memory.

Is using strlen() enough to prevent buffer overflow?

No. strlen() only measures the source string length and provides no protection against overflow. You must compare the source length against the destination buffer size or use strnlen() with the buffer size as the maximum.

Can static analysis detect buffer overflow in memcpy?

Yes. Static analysis tools can detect patterns where memcpy uses strlen() of the source without bounds checking against the destination size, flagging potential overflow conditions at compile time.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #111

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.