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
Published May 24, 2026Reviewed 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

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

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.


Prevention & Best Practices

1. Never Trust Externally-Supplied Lengths or Offsets

Any value that crosses a trust boundary — from a network packet, USB command, file header, or user input — must be validated before use in memory operations. This is non-negotiable.

// ❌ Dangerous: trusting external data directly
memcpy(dst, src + external_offset, external_size);

// ✅ Safe: validate first
if (external_size > MAX_SIZE || external_offset > MAX_SIZE - external_size) {
    return ERROR_INVALID_PARAMETER;
}
memcpy(dst, src + external_offset, external_size);

2. Use Safe Memory Functions Where Available

Modern C libraries offer safer alternatives:
- memcpy_s() (C11 Annex K) — takes a destination buffer size
- memmove_s() for overlapping regions
- In C++, prefer std::copy with iterators that encode bounds

// C11 bounds-checking version
errno_t err = memcpy_s(dst, dst_size, src + offset, bufsize);
if (err != 0) { /* handle error */ }

3. Define and Enforce Security Invariants in Code

Write your invariants as assertions or early-return guards at the top of functions:

bool processSCSIRead(uint32_t offset, uint32_t bufsize) {
    // Security invariant: access must be within sector buffer
    assert(bufsize <= SECTOR_SIZE);
    assert(offset <= SECTOR_SIZE - bufsize);

    // ... rest of function
}

4. Enable Compiler and Runtime Protections

For embedded targets where possible:
- Stack canaries (-fstack-protector-strong)
- AddressSanitizer during development/testing (-fsanitize=address)
- Undefined Behavior Sanitizer (-fsanitize=undefined) catches integer overflows
- Static analysis: tools like cppcheck, clang-tidy, or Coverity can flag unchecked buffer operations

# Build with sanitizers for testing
gcc -fsanitize=address,undefined -g -o firmware_test test.c

5. Fuzz Your Protocol Parsers

Any code that processes structured binary input (SCSI commands, network packets, file formats) should be fuzz-tested. Tools like AFL++ or libFuzzer are excellent at generating the exact kind of edge-case inputs that trigger bounds violations:

# Example: fuzz the SCSI command handler
afl-fuzz -i scsi_seeds/ -o findings/ -- ./scsi_handler_harness @@

6. Apply the Principle of Least Privilege to USB

If your device doesn't need to support arbitrary SCSI commands from a host, restrict which commands are accepted. Validate the command opcode, LBA range, and transfer length against known-good values before processing.

Relevant Standards and References


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.

Frequently Asked Questions

What is an out-of-bounds write vulnerability?

An out-of-bounds write occurs when a program writes data beyond the allocated boundary of a buffer. In C, this typically happens when an index, offset, or size value is not validated before being used in a memcpy, strcpy, or direct array write, allowing an attacker to corrupt adjacent memory.

How do you prevent out-of-bounds writes in C embedded firmware?

Always validate that `offset + size <= buffer_length` before any memcpy or array write operation, especially when the offset or size originates from external input such as USB, network, or serial data. Use static analysis tools and enable compiler sanitizers (AddressSanitizer, UBSan) during development.

What CWE is an out-of-bounds write?

Out-of-bounds write is classified as CWE-787. It is consistently ranked in the MITRE CWE Top 25 Most Dangerous Software Weaknesses and is a leading cause of exploitable memory corruption vulnerabilities.

Is input length checking alone enough to prevent this type of buffer overflow?

No. You must check both the offset and the combined offset+size against the buffer boundary. Checking only the size allows an attacker to set a valid size but a large offset that causes the write to start inside the buffer and extend past its end.

Can static analysis detect this type of out-of-bounds vulnerability?

Yes. Tools like Semgrep, Coverity, CodeChecker, and clang-tidy can flag memcpy calls where size or offset parameters are derived from external data without prior bounds validation. Orbis AppSec detected this exact pattern automatically in the FatFSUSB library.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #109

Related Articles

critical

How buffer overflow happens in C tar header parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `microtar/microtar.c` where the `raw_to_header()` and `header_to_raw()` functions used unbounded `strcpy()` and `sprintf()` calls to copy tar header fields. Malicious tar files with non-null-terminated name fields could overflow destination buffers, potentially leading to code execution. The fix replaces all unsafe string operations with bounded alternatives: `memcpy()` with explicit null-termination and `snprintf()` instead of `sprintf(

critical

How buffer overflow happens in C ieee80211_input() and how to fix it

A critical buffer overflow vulnerability was discovered in `src/firmware/src/net/ieee80211.c` at line 1584, where the `ieee80211_input()` function processed raw 802.11 data frames without verifying that the incoming frame was large enough to contain a valid `ieee80211_frame` header. An attacker within wireless range could craft undersized or malformed frames to trigger memory corruption, potentially leading to remote code execution on the firmware. The fix adds a single, targeted bounds check th

high

How buffer overflow from unsafe string copy functions happens in C network interface code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `generic/eth-impl.c`, where unsafe `strncpy()` and `sprintf()` calls could write beyond buffer boundaries when handling network interface names and device filenames. The fix replaced these dangerous functions with bounded `snprintf()` calls that guarantee null-termination and prevent memory corruption.

critical

How buffer overflow in FuzzIxml.c sprintf() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `fuzzer/FuzzIxml.c` where `sprintf()` wrote a PID-formatted filename into a fixed 256-byte stack buffer without any bounds checking. The fix replaces `sprintf()` with `snprintf()`, explicitly passing the buffer size to prevent any overflow. While exploitation in this specific fuzzer context requires local access, the pattern is a textbook example of CWE-120 that developers should recognize and eliminate everywhere it appears.

critical

How buffer overflow happens in C HTML parsing and how to fix it

A critical buffer overflow vulnerability in `include/html_parse.h` allowed attackers to overflow buffers by providing malicious HTML input exceeding buffer capacity. The fix adds proper bounds checking before memcpy() operations to prevent memory corruption and potential code execution.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.