Back to Blog
critical SEVERITY8 min read

Buffer Overflow via Crafted SCSI Commands: How a Missing Bounds Check Almost Bricked Your ESP32

A critical out-of-bounds memory access vulnerability was discovered in the FatFSUSB library used by the micro-journal ESP32 firmware, where memcpy operations on a sector buffer accepted attacker-controlled offset and size values from USB SCSI commands without any bounds validation. A malicious USB host could craft SCSI READ/WRITE commands to corrupt memory, potentially crashing the device or executing arbitrary code. The fix adds a simple but essential bounds check before every memcpy operation,

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 24, 2026•Reviewed June 3, 2026

Answer Summary

This is a critical out-of-bounds write vulnerability (CWE-787) in C firmware for the ESP32 microcontroller, specifically in the FatFSUSB library's handling of USB SCSI READ/WRITE commands. The root cause is that `memcpy` operations on a fixed-size sector buffer used attacker-controlled `offset` and `size` values derived directly from incoming SCSI command data, with no validation that `offset + size <= SECTOR_SIZE`. The fix adds a simple bounds check — `if (offset + size > SECTOR_SIZE) return error;` — before every `memcpy`, preventing any USB host from writing outside the buffer. Developers working with USB MSC (Mass Storage Class) stacks in embedded C should treat all SCSI command fields as untrusted input.

Vulnerability at a Glance

cweCWE-787
fixAdded bounds check (offset + size <= SECTOR_SIZE) before every memcpy on the sector buffer
riskMemory corruption, device crash, potential arbitrary code execution triggered by a malicious USB host
languageC (ESP32 Arduino/IDF firmware)
root causememcpy on a fixed-size sector buffer used attacker-controlled offset and size from USB SCSI commands without bounds validation
vulnerabilityOut-of-bounds write via unchecked SCSI command fields

Introduction

Imagine plugging your custom ESP32-based device into a computer — maybe it's a niche writing gadget, a portable logger, or a DIY data recorder — and the USB host quietly sends a specially crafted SCSI command that overwrites critical memory on your microcontroller. No warning. No crash dialog. Just silent corruption, a potential device brick, or worse: arbitrary code execution on hardware you thought you controlled.

This is not a hypothetical. A high-severity out-of-bounds memory access vulnerability was recently discovered and patched in the FatFSUSB library, a component that enables ESP32 devices to appear as USB Mass Storage devices to a connected host computer. The root cause? A missing bounds check before a memcpy — one of the oldest and most well-documented classes of bugs in systems programming.

If you're writing firmware, embedded C/C++, or any code that processes externally-supplied length and offset values, this post is for you.


The Vulnerability Explained

What Is FatFSUSB?

FatFSUSB is a library that bridges the FatFS filesystem (common in embedded systems) with the USB Mass Storage Class (MSC) protocol. When your ESP32 device is connected via USB, a host computer can issue SCSI commands — standard block-level read/write operations — to access the device's filesystem as if it were a USB thumb drive.

The library maintains an internal sector buffer (_sectBuff) of a fixed size (typically 512 bytes, matching a standard disk sector). When the host requests a read or write, the library copies data to or from this buffer using memcpy.

The Vulnerable Code

At lines 130, 243, and 253 of FatFSUSB.cpp, the code performed operations similar to:

// BEFORE (vulnerable)
// 'offset' and 'bufsize' come directly from SCSI command parameters
memcpy(buffer, _sectBuff + offset, bufsize);
// ... and for writes:
memcpy(_sectBuff + offset, buffer, bufsize);

The critical problem: offset and bufsize are derived directly from SCSI command fields sent by the USB host. There is no validation that offset + bufsize stays within the bounds of _sectBuff.

How Could It Be Exploited?

The USB Mass Storage Class protocol is designed to be spoken by a trusted host operating system. But "trusted" is a dangerous assumption. Consider these realistic attack scenarios:

Scenario 1: Malicious Host Software
A piece of malware running on the connected computer gains control of the USB stack and issues crafted SCSI WRITE commands with a valid-looking sector address but an oversized bufsize. The memcpy happily writes attacker-controlled data past the end of _sectBuff, corrupting adjacent heap or stack memory.

Scenario 2: Rogue USB Host Device
A Raspberry Pi or other microcontroller masquerading as a USB host (using USB OTG) sends crafted SCSI commands directly. This is a physical-access attack vector but highly realistic for devices that might be left unattended, like a shared writing device in a library or office.

Scenario 3: Integer Overflow Wraparound
An attacker sends offset = 0xFFFFFFFF and bufsize = 2. On a 32-bit system, the unchecked addition offset + bufsize wraps around to 1, which looks safe — but the pointer arithmetic _sectBuff + 0xFFFFFFFF points to a wildly out-of-bounds memory location.

What's the Real-World Impact?

On a microcontroller like the ESP32, memory is a precious and tightly-packed resource. The heap, stack, and various peripheral control structures often live in close proximity. An out-of-bounds write into this space can:

  • Crash the device (best case — a watchdog reset)
  • Corrupt filesystem metadata, causing data loss
  • Overwrite function pointers or return addresses, enabling code execution
  • Disable security features by overwriting configuration flags in memory

This vulnerability is classified under CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer and is related to CWE-787 (Out-of-bounds Write) and CWE-125 (Out-of-bounds Read).


The Fix

What Changed

The fix is conceptually simple but critically important: validate that offset + bufsize does not exceed the size of _sectBuff before calling memcpy.

// AFTER (fixed)
// Validate bounds before any memcpy operation
if (offset < 0 || bufsize < 0 || (offset + bufsize) > SECTOR_SIZE) {
    // Reject the malformed SCSI command gracefully
    return false; // or signal an error to the USB stack
}
memcpy(buffer, _sectBuff + offset, bufsize);

And for write operations:

// AFTER (fixed) - write path
if (offset < 0 || bufsize < 0 || (offset + bufsize) > SECTOR_SIZE) {
    return false;
}
memcpy(_sectBuff + offset, buffer, bufsize);

Why This Works

The fix enforces a security invariant: the memory region accessed by memcpy must always fall entirely within the allocated buffer. Let's break down each check:

Check What It Prevents
offset < 0 Negative offset used as a large unsigned pointer offset
bufsize < 0 Negative size interpreted as a huge unsigned value
offset + bufsize > SECTOR_SIZE Combined overflow beyond buffer end

Note that on systems where these are unsigned integers, the negative checks may be replaced with overflow-safe arithmetic (e.g., checking bufsize > SECTOR_SIZE - offset to avoid the addition itself overflowing).

The Integer Overflow Subtlety

A naive check like if (offset + bufsize <= SECTOR_SIZE) can itself be vulnerable if offset and bufsize are unsigned 32-bit integers and their sum wraps around. The safer pattern:

// Safe against unsigned integer overflow
if (bufsize > SECTOR_SIZE || offset > SECTOR_SIZE - bufsize) {
    return false; // reject
}

This avoids ever computing the potentially-overflowing sum directly.


The Regression Test Suite

The fix was accompanied by a comprehensive Python test suite that validates the security invariant. Here's what makes it exemplary:

def validate_scsi_bounds(offset, bufsize, sector_size=SECTOR_SIZE):
    """
    Security invariant: offset + bufsize must never exceed sector_size.
    """
    if offset < 0 or bufsize < 0:
        return False
    return (offset + bufsize) <= sector_size

The tests cover:
- Clearly out-of-bounds cases: offset=0, bufsize=513
- Subtle off-by-one attacks: offset=511, bufsize=2
- Integer overflow/wraparound: offset=2^32-10, bufsize=20
- Negative values: offset=-1, bufsize=512
- Valid cases that must still work: offset=0, bufsize=512 (exact fit is fine)

This last category is crucial — security fixes that break legitimate functionality are just as harmful as the vulnerability itself.


Conclusion

A single missing bounds check — a few lines of validation code — was all that stood between a well-intentioned embedded library and a remotely-exploitable memory corruption vulnerability. This is the nature of systems programming: the rules are unforgiving, and the consequences of trusting external input without verification can be severe.

The key lessons from this vulnerability:

  1. Every external input is potentially hostile. SCSI commands from a USB host are no different from HTTP requests from the internet — validate them.
  2. Buffer arithmetic is dangerous. Always check that offset + size cannot exceed your buffer, and be aware of integer overflow in the check itself.
  3. Simple fixes can close serious holes. The patch here is a handful of lines, but it eliminates an entire class of memory corruption attacks.
  4. Write tests that encode your security invariants. The regression tests included with this fix will catch any future regression that accidentally removes the bounds check.

Embedded and firmware developers often operate under the assumption that their devices are in a "trusted" environment. As USB-connected devices become more prevalent — and as attacks on embedded systems grow more sophisticated — that assumption is increasingly dangerous. Treat every external interface as a potential attack surface, validate all inputs, and let your compiler and sanitizers help you catch mistakes early.

Stay safe, and keep your buffers bounded. 🔒


This vulnerability was identified and fixed as part of an automated security scanning pipeline. The fix was verified with a full re-scan and LLM-assisted code review before merging.

Prevention and further reading

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

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

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.