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

high

How heap buffer overflow happens in C JMA archive extraction and how to fix it

A heap buffer overflow vulnerability in `jma/jma.cpp` allowed a crafted JMA ROM archive to trigger out-of-bounds memory writes during file extraction. The flaw existed at line 446, where `memcpy` was called with `first_chunk_offset` and `copy_amount` values derived directly from archive header metadata without any validation that those values stayed within the bounds of either the source or destination buffer. The fix adds a pre-copy bounds check that rejects malformed archives before the danger

critical

How unsafe buffer copying happens in C credential storage and how to fix it

A critical vulnerability in `lib/server.c` allowed attackers to trigger out-of-bounds memory reads when copying credentials via unsafe `memcpy()` calls. By replacing `memcpy()` with bounds-safe `strlcpy()`, the fix ensures credentials are safely stored without buffer overruns or null-termination issues.

critical

How buffer overflow happens in C Bluetooth device handling and how to fix it

A critical buffer overflow vulnerability in `src/wiiuse.c` allowed attackers within Bluetooth range to trigger heap corruption by sending specially crafted HID packets with oversized length values. The fix adds strict bounds checking to validate that data lengths don't exceed buffer capacity before performing memory operations, preventing exploitation by malicious or intercepted Bluetooth devices.

critical

How buffer overflow happens in C patches.c sprintf macros and how to fix it

A critical buffer overflow vulnerability was discovered in `src/patches.c` where the `_EPRINT_I`, `_EPRINT_F`, and `_EPRINT_COEF` macros used `sprintf()` to write formatted AMY event data into a fixed-size buffer without any bounds checking. By replacing every `sprintf()` call with `snprintf()` and tracking remaining buffer space using a `s_entry` base pointer, the fix ensures that formatting 22 event fields — even at maximum values — can never write beyond the buffer boundary.

critical

How buffer overflow happens in C dcraw_lz.c nikon_3700() and how to fix it

A critical buffer overflow vulnerability was discovered in `lightcrafts/coprocesses/dcraw/dcraw_lz.c` at line 1334, where the `nikon_3700()` function used `strcpy()` to copy camera make and model strings into fixed 64-byte buffers without any bounds checking. A crafted RAW image file with oversized make/model metadata could trigger a heap or stack corruption, potentially enabling arbitrary code execution. The fix replaces both `strcpy()` calls with `strncpy()` and explicit null-termination, enfo

critical

How SQL injection happens in PostgreSQL dictionary synchronization and how to fix it

A critical SQL injection vulnerability in `zhparser--2.1.sql` allowed attackers to execute arbitrary SQL commands by crafting malicious database names. The vulnerability existed because the dictionary synchronization function constructed COPY commands using string concatenation without proper escaping. This fix implements parameterized queries to safely handle database identifiers.