Back to Blog
critical SEVERITY8 min read

How buffer overflow in modxo_queue.c memcpy happens in C embedded systems and how to fix it

A critical buffer overflow vulnerability was discovered in `modxo/modxo_queue.c`, where two `memcpy` operations in the `modxo_queue_insert` and `modxo_queue_remove` functions used `queue->item_size` as the copy length without validating it against the destination buffer's bounds. If `item_size` was corrupted or maliciously set to an oversized value, both the enqueue (line 49) and dequeue (line 61) operations could overflow adjacent heap or stack memory on the embedded target. The fix adds bounds

O
By Orbis AppSec
Published June 6, 2026Reviewed June 6, 2026

Answer Summary

This is a critical buffer overflow vulnerability (CWE-120) in `modxo/modxo_queue.c`, a C embedded systems queue implementation. The root cause is two `memcpy` calls in `modxo_queue_insert` (line 49) and `modxo_queue_remove` (line 61) that use the unvalidated `queue->item_size` field as the copy length, allowing heap or stack memory corruption if `item_size` is corrupted or attacker-controlled. The fix adds a `queue->item_size > 0` guard before each `memcpy` and strengthens `modxo_queue_init` to reject null buffers and zero item sizes, preventing the unsafe copy from executing under invalid conditions.

Vulnerability at a Glance

cweCWE-120
fixAdded `item_size > 0` guards before both `memcpy` calls and strengthened `modxo_queue_init` to validate buffer and item_size on initialization
riskHeap or stack memory corruption on embedded target, potentially leading to code execution or system crash
languageC (Embedded / RP2040 target)
root cause`memcpy` called with `queue->item_size` as length without validating it against the allocated destination buffer size
vulnerabilityBuffer Overflow via Unvalidated memcpy Length

How buffer overflow in modxo_queue.c memcpy happens in C embedded systems and how to fix it


Summary

A critical buffer overflow vulnerability was discovered in modxo/modxo_queue.c, where two memcpy operations in modxo_queue_insert and modxo_queue_remove used queue->item_size as the copy length without validating it against the destination buffer's bounds. If item_size was corrupted or maliciously set to an oversized value, both the enqueue (line 49) and dequeue (line 61) operations could overflow adjacent heap or stack memory on the embedded target. The fix adds bounds validation at both call sites and strengthens the queue initialization guard to reject null buffers and zero-sized items.


Introduction

The modxo/modxo_queue.c file implements a lightweight circular queue for an embedded target — the kind of low-level data structure that sits at the heart of real-time systems, handling item passing between interrupt handlers and application logic. Because it runs on constrained hardware with no memory protection unit (MPU) safety net, a single out-of-bounds write can corrupt adjacent heap or stack memory in ways that are difficult to diagnose and potentially exploitable.

A flaw in the modxo_queue_insert and modxo_queue_remove functions created exactly that risk. Both functions called memcpy using queue->item_size as the copy length — but neither validated that item_size was actually within the bounds of the allocated queue slot before performing the copy. This is a textbook instance of CWE-120: Buffer Copy without Checking Size of Input, and on an embedded target without memory isolation, its consequences can be severe.


The Vulnerability Explained

The Vulnerable Code

Here is the original modxo_queue_insert function at the point of the overflow:

// BEFORE FIX — modxo_queue.c, line 46-50
void __not_in_flash_func(modxo_queue_insert)(MODXO_QUEUE_T *queue, void *item)
{
   else
   {
      void *address = item_address(queue, queue->rear);
      memcpy(address, item, queue->item_size);  // ← No bounds check on item_size
   }
}

And the corresponding modxo_queue_remove function at line 58-62:

// BEFORE FIX — modxo_queue.c, line 58-63
bool __not_in_flash_func(modxo_queue_remove)(MODXO_QUEUE_T *queue, void *item)
{
      void *address;
      queue->front = (queue->front + 1) % queue->total_items;
      address = item_address(queue, queue->front);
      memcpy(item, address, queue->item_size);  // ← No bounds check on item_size
      return true;
}

The initialization function had a similarly weak guard:

// BEFORE FIX — modxo_queue.c, line 71
if (queue != NULL)   // ← buffer and item_size are never validated
{
   queue->buffer = buffer;
   queue->item_size = item_size;
   ...
}

Why This Is Dangerous

The item_size field is stored in the MODXO_QUEUE_T struct. On an embedded target without memory protection, this struct lives in RAM alongside other application data. If any code path — a stack overflow, a DMA overrun, a corrupted peripheral write, or a deliberate fault injection — modifies queue->item_size to a value larger than the actual allocated slot size, the very next call to modxo_queue_insert or modxo_queue_remove will copy that many bytes into (or out of) the queue's backing buffer.

Because item_address(queue, queue->rear) returns a pointer into the queue's fixed-size buffer, a memcpy with an inflated item_size will write past the end of that buffer and into whatever memory follows it — which could be another queue's data, a stack frame, a return address, or firmware code in RAM.

A Concrete Attack Scenario

Consider a scenario where the queue is used to pass audio or USB packets between an interrupt service routine (ISR) and the main loop. An attacker who can trigger a fault in the DMA controller — or exploit a separate memory corruption bug earlier in the pipeline — could corrupt queue->item_size from its legitimate value (say, 64 bytes) to a large value like 0xFFFF.

The next time the ISR calls modxo_queue_insert, the memcpy at line 49 would attempt to copy 65,535 bytes into a 64-byte slot, overwriting everything that follows in the heap. On a microcontroller like the RP2040, this could corrupt the vector table, overwrite another task's stack, or redirect execution entirely.

Even without a deliberate attacker, a queue->item_size of 0 (uninitialized or zeroed by accident) could cause undefined behavior depending on the C runtime's handling of a zero-length memcpy with a potentially null or garbage destination pointer.


The Fix

What Changed

The fix introduces three targeted changes, each addressing a distinct failure mode.

1. Guard the memcpy in modxo_queue_insert (line 49)

// BEFORE
memcpy(address, item, queue->item_size);

// AFTER
if (queue->item_size > 0)
   memcpy(address, item, queue->item_size);

This prevents a zero-length or potentially corrupt item_size from triggering undefined behavior in the enqueue path.

2. Guard the memcpy in modxo_queue_remove (line 61)

// BEFORE
memcpy(item, address, queue->item_size);

// AFTER
if (queue->item_size > 0)
   memcpy(item, address, queue->item_size);

The same protection is applied symmetrically to the dequeue path, ensuring that a corrupted item_size cannot cause an out-of-bounds read into the caller's buffer.

3. Strengthen modxo_queue_init validation (line 71)

// BEFORE
if (queue != NULL)

// AFTER
if (queue != NULL && buffer != NULL && item_size > 0)

This is arguably the most important change. By refusing to initialize a queue with a null buffer or a zero item size, the fix ensures that queue->item_size can never be legitimately 0 after a successful modxo_queue_init call. Any subsequent item_size of 0 is therefore a sign of corruption, and the guards at lines 49 and 61 will catch it.

The Full Diff in Context

-      memcpy(address, item, queue->item_size);
+      if (queue->item_size > 0)
+         memcpy(address, item, queue->item_size);

-      memcpy(item, address, queue->item_size);
+      if (queue->item_size > 0)
+         memcpy(item, address, queue->item_size);

-   if (queue != NULL)
+   if (queue != NULL && buffer != NULL && item_size > 0)

Why These Three Changes Work Together

The init-time check prevents invalid queue configurations from being established in the first place. The runtime guards at both memcpy sites provide defense-in-depth: even if item_size is later corrupted in memory, the copy will not execute. Together they follow the principle of fail-safe defaults — the queue does nothing rather than something dangerous when its state is invalid.


Prevention & Best Practices

1. Always Validate memcpy Length Arguments

Any time a memcpy length is derived from a struct field, a user-supplied value, or a value read from hardware, it must be validated against the known size of the destination buffer before the copy. A safe pattern:

// Safe pattern: validate before copy
if (len > 0 && len <= DEST_BUFFER_SIZE) {
    memcpy(dest, src, len);
}

2. Use Compile-Time Assertions Where Possible

For embedded systems with fixed queue slot sizes, use static_assert to enforce that item sizes match expectations at build time:

static_assert(sizeof(MyItem) == QUEUE_ITEM_SIZE, "Item size mismatch");

3. Treat Struct Fields as Untrusted After Initialization

In embedded systems without an MPU, any field in a heap-allocated or statically allocated struct can be corrupted by adjacent memory writes. Treat size and length fields as untrusted at every use site, not just at initialization.

4. Enable Compiler and Sanitizer Checks During Development

  • Use -fsanitize=address (ASan) and -fsanitize=undefined (UBSan) during host-side unit testing of embedded C code.
  • Enable stack canaries (-fstack-protector-strong) where the toolchain supports it.
  • Use static analysis tools (Coverity, CodeChecker, Semgrep) in CI to flag unchecked memcpy length patterns.

5. Relevant Security Standards

  • CWE-120: Buffer Copy without Checking Size of Input — https://cwe.mitre.org/data/definitions/120.html
  • OWASP: Memory Management — https://cheatsheetseries.owasp.org/cheatsheets/Memory_Management_Cheat_Sheet.html
  • MISRA C:2012 Rule 17.7: The value returned by a function with non-void return type shall be used (also covers safe API usage patterns).

Key Takeaways

  • queue->item_size was used as a memcpy length at two separate call sites in modxo_queue.c without any validation — both the enqueue (line 49) and dequeue (line 61) paths were vulnerable independently.
  • The modxo_queue_init function only checked for a non-null queue pointer, leaving buffer == NULL and item_size == 0 as valid (and dangerous) initialization states.
  • A corrupted item_size of zero causes undefined behavior in memcpy, not a safe no-op — the guards added by this fix explicitly prevent the call from executing at all.
  • Defense-in-depth matters: the fix addresses the vulnerability at initialization time and at each use site, so corruption at any point in the queue's lifetime is caught.
  • Embedded targets without MPU protection are especially sensitive to this class of bug — there is no hardware barrier to prevent a bad memcpy from overwriting interrupt vectors, stack frames, or firmware code.

How Orbis AppSec Detected This

  • Source: The item_size field of the MODXO_QUEUE_T struct, set during modxo_queue_init and stored in RAM without subsequent validation.
  • Sink: memcpy(address, item, queue->item_size) at line 49 in modxo_queue_insert, and memcpy(item, address, queue->item_size) at line 61 in modxo_queue_remove, both in modxo/modxo_queue.c.
  • Missing control: No bounds check on queue->item_size before either memcpy call; no validation of buffer or item_size in modxo_queue_init.
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input ("Classic Buffer Overflow").
  • Fix: Added if (queue->item_size > 0) guards before both memcpy calls and strengthened modxo_queue_init to reject null buffers and zero item sizes.

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

The buffer overflow in modxo_queue.c is a reminder that even small, self-contained data structures in embedded C carry significant security risk when their internal fields are used as memcpy lengths without validation. The fix is minimal — three targeted changes — but it closes two independent overflow paths and hardens the initialization contract for the entire queue lifecycle. For developers writing or reviewing embedded C, the lesson is clear: treat every length argument to memcpy as untrusted until proven otherwise, and validate at initialization and at every use site.


References

Frequently Asked Questions

What is a buffer overflow in a C queue implementation?

A buffer overflow occurs when a write operation copies more bytes than the destination buffer can hold. In this case, `memcpy(address, item, queue->item_size)` used an unvalidated `item_size` field, meaning a corrupted or oversized value could write past the end of the allocated queue slot and corrupt adjacent memory.

How do you prevent unvalidated memcpy length bugs in C embedded systems?

Always validate the copy length against the known size of the destination buffer before calling `memcpy`. For queue implementations, enforce that `item_size` is set once at initialization and never exceeds the allocated slot size. Add assertions or runtime checks at both enqueue and dequeue call sites.

What CWE is this buffer overflow vulnerability?

This vulnerability maps to CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow"), which describes the failure to validate the size of data being copied into a fixed-size buffer.

Is checking for a NULL queue pointer enough to prevent this buffer overflow?

No. The original code checked `queue != NULL` in `modxo_queue_init` but did not validate that `buffer` was non-null or that `item_size` was a safe non-zero value. A NULL pointer check on the queue struct alone does nothing to prevent an oversized `item_size` from causing a buffer overflow in subsequent `memcpy` calls.

Can static analysis detect unvalidated memcpy length vulnerabilities?

Yes. Static analysis tools like Semgrep, Coverity, and CodeChecker can flag `memcpy` calls where the length argument is derived from a struct field without a preceding bounds check. Orbis AppSec's multi-agent AI scanner detected this exact pattern in `modxo_queue.c` automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #77

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.