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

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #109

Related Articles

critical

Heap Buffer Overflow in Audio Ring Buffer: How a Missing Bounds Check Could Crash Your App

A critical heap buffer overflow vulnerability was discovered in `audio_backend.c`, where the audio ring buffer's `memcpy` operations lacked bounds validation before writing PCM data. Without checking that incoming data sizes fell within the allocated buffer's capacity, a maliciously crafted audio file could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix adds a concise pre-flight validation guard that rejects out-of-range write requests before any memory oper

critical

Critical Heap Buffer Overflow in SSDP Control Point: How Unbounded String Operations Put Networks at Risk

A critical heap buffer overflow vulnerability was discovered and patched in the SSDP control point implementation (`ssdp_ctrlpt.c`), where multiple unbounded `strcpy` and `strcat` operations constructed HTTP request buffers without any length validation. Network-received SSDP response fields — including service type strings and location URLs — could be crafted by an attacker to exceed buffer boundaries, potentially enabling arbitrary code execution or denial of service. The fix replaces the unsa

critical

Heap Buffer Overflow in OPDS Parser: How a Misplaced Variable Nearly Opened the Door to Remote Code Execution

A critical heap buffer overflow vulnerability was discovered in `lib/OpdsParser/OpdsParser.cpp`, where the buffer allocation size was calculated *after* a fixed chunk size was used to allocate memory, meaning the actual bytes read could exceed the allocated buffer. On embedded devices parsing untrusted OPDS catalog data from the network, this flaw could allow a remote attacker to corrupt heap memory and potentially achieve arbitrary code execution. The fix was elegantly simple: move the `toRead`

critical

Heap Buffer Overflow in BLE MIDI: How a Missing Bounds Check Opens the Door to Remote Exploitation

A critical heap buffer overflow vulnerability was discovered in the BLE MIDI packet assembly code of `blemidi.c`, where attacker-controlled packet length values could trigger writes beyond allocated heap memory. The fix adds an integer overflow guard before the `malloc` call, ensuring that maliciously crafted BLE MIDI packets can no longer corrupt heap memory. This vulnerability is particularly dangerous because it is remotely exploitable by any nearby Bluetooth device — no physical access requi

critical

Heap Overflow in TOML Parser: How Integer Overflow Leads to Memory Corruption

A critical heap buffer overflow vulnerability was discovered and patched in the centitoml TOML parser, where missing integer overflow validation on a `MALLOC(len+1)` call could allow an attacker to trigger memory corruption via a crafted TOML configuration file. The vulnerability (CWE-190) is reachable through community-distributed mod or map files that the game loads from its `config/` directory, making it a realistic attack vector for remote code execution. A targeted one-line guard now preven

critical

Heap Corruption via Unchecked memcpy: How Integer Overflow Bugs Corrupt Memory in Windows File Operations

A critical buffer overflow vulnerability was discovered in `phlib/nativefile.c`, where multiple `memcpy` calls copied filename and extended-attribute data into fixed-size structures without verifying that source lengths didn't exceed destination buffer boundaries. An attacker supplying an oversized filename or EA name could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix replaces unchecked arithmetic with Windows' safe integer helpers (`RtlULongAdd`, `RtlULon