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 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

high

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

A high-severity buffer overflow vulnerability was discovered in `tools/claude-vscode-wrapper.c`, where an unbounded `strcpy()` call copied a file path into a fixed-size `MAX_PATH` buffer without any size validation. The fix replaces `strcpy()` with `snprintf()` and swaps `malloc()` for `calloc()`, ensuring both string operations and memory allocation are bounds-safe and zero-initialized.

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.

critical

How Stack Buffer Overflows Happen in C with sprintf() and How to Fix Them

A critical stack buffer overflow was discovered in `libuv/Learn-libuv/docs/code/tty-gravity/main.c` where `sprintf()` wrote ANSI escape sequences and user-controlled variables into a fixed 500-byte buffer without any bounds checking. An attacker controlling the `pos`, `width`, or `message` variables could overflow the stack, overwrite return addresses, and potentially achieve arbitrary code execution. The fix replaces `sprintf()` with `snprintf()` and adds explicit length validation to ensure wr

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.