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 buffer overflow in URL parsing happens in C++ HTTP client and how to fix it

A critical buffer overflow vulnerability in the HTTP client's URL parsing function allowed attackers to overflow a stack-allocated host buffer through specially crafted URLs with excessively long hostnames. The vulnerability enabled arbitrary code execution by overwriting the return address. The fix adds proper bounds validation before the memcpy() operation to ensure the hostname length never exceeds the destination buffer size.

critical

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

A critical integer overflow vulnerability in `include/compat/dirent_msvc.h` allowed an attacker-controlled directory path length to wrap the `sizeof(wchar_t) * n + 16` allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.

critical

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.

high

How Denial of Service via crafted URI templates happens in Ruby addressable and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-35611) was discovered in the Ruby `addressable` gem versions prior to 2.9.0, which could allow attackers to crash or hang applications by sending specially crafted URI templates. The fix upgrades the dependency from version 2.8.7 to 2.9.0 across the Gemfile, Gemfile.lock, and gemspec in a Fastlane project, eliminating the vulnerable code path entirely.

critical

How Server-Side Request Forgery (SSRF) happens in Python requests.get() and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `models/common.py` where `requests.get()` fetched images from arbitrary URLs without validating whether the target resolved to internal infrastructure. An attacker could supply URLs targeting AWS metadata endpoints (169.254.169.254), private networks, or localhost services through the Flask REST API. The fix introduces DNS-resolution-based validation using Python's `socket.getaddrinfo()` and `ipaddress` module to block

critical

How heap buffer overflow happens in C WiFi frame capture and how to fix it

A critical buffer overflow vulnerability in the ESP32 WiFi frame capture feature (feat_capture_hs.c) allowed attackers within WiFi range to craft oversized 802.11 frames that would overflow heap buffers and achieve remote code execution. The fix adds explicit length validation before memcpy operations and rejects oversized frames rather than silently truncating them.