Back to Blog
critical SEVERITY9 min read

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.

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

Answer Summary

This is a classic C buffer overflow (CWE-121, Stack-based Buffer Overflow) in `src/patches.c` where the `_EPRINT_I` and `_EPRINT_F` macros used `sprintf()` to cumulatively write all AMY event fields into a fixed-size buffer with no bounds checking. An attacker sending crafted AMY wire protocol messages with all fields set to maximum values could overflow the buffer and potentially achieve arbitrary code execution. The fix replaces every `sprintf()` with `snprintf(s, len - (size_t)(s - s_entry), ...)`, passing the remaining buffer space as the size argument so each write is bounded. Developers working with similar event-serialization patterns in C should always track remaining buffer capacity when using pointer-advancing write loops.

Vulnerability at a Glance

cweCWE-121
fixReplace all `sprintf()` calls with `snprintf(s, len - (size_t)(s - s_entry), ...)` to enforce bounds on every write
riskCrafted AMY wire protocol input can overflow a stack buffer, potentially enabling arbitrary code execution or denial of service
languageC
root cause`_EPRINT_I`, `_EPRINT_F`, and `_EPRINT_COEF` macros advance a pointer `s` with `sprintf()` and `strlen()` but never check remaining buffer capacity
vulnerabilityStack-based Buffer Overflow via unbounded sprintf() in event-formatting macros

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

Summary

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.


Introduction

The src/patches.c file handles serialization of AMY synthesizer events — converting internal event structures into formatted strings for both human-readable debug output and wire protocol transmission. At line 160, a set of three macros — _EPRINT_I, _EPRINT_F, and _EPRINT_COEF — do the heavy lifting of writing each event field into a shared buffer s. The problem: every single one of those writes used sprintf(), which has no concept of how much space remains in the buffer.

This is a textbook example of a vulnerability that looks harmless in isolation but becomes critical when you count the fields. AMY events have 22 printable fields: 12 integers (freq, midi_note, velocity, patch, wave, osc, time, channel, bp0bp3) and 10 floats (amp, phase, feedback, filter_freq, filter_type, resonance, pan, pitch_bend, ratio, detune), plus coefficient arrays. With all fields set to maximum values, the cumulative output easily overflows any reasonably sized stack buffer.


The Vulnerability Explained

The Vulnerable Macro Pattern

Here is the exact code that was vulnerable, at src/patches.c:160:

// BEFORE — vulnerable
#define _EPRINT_I(FIELD, NAME, WIRECODE) \
    if (AMY_IS_SET(e->FIELD)) { \
        sprintf(s, "%s%" PRId32, wirecode ? WIRECODE : " " NAME ": ", (int32_t)e->FIELD); \
        s += strlen(s); \
    }

#define _EPRINT_F(FIELD, NAME, WIRECODE) \
    if (AMY_IS_SET(e->FIELD)) { \
        sprintf(s, "%s%.3f", wirecode ? WIRECODE : " " NAME ": ", e->FIELD); \
        s += strlen(s); \
    }

And the coefficient array macro:

// BEFORE — vulnerable
#define _EPRINT_COEF(FIELD, NAME, WIRECODE) {
    ...
    if (last_set >= 0) {
        sprintf(s, "%s", wirecode ? WIRECODE : " " NAME ": ");
        s += strlen(s);
        for (int i = 0; i <= last_set; ++i) {
            if (i > 0) { sprintf(s, ","); s += strlen(s); }
            if (AMY_IS_SET(e->FIELD[i])) {
                sprintf(s, "%.3f", e->FIELD[i]);
                s += strlen(s);
            }
        }
    }
}

Why This Is Dangerous

The critical flaw is the combination of two patterns:

  1. sprintf() writes without a size limit — it will write as many bytes as the formatted output requires, regardless of how much buffer space remains.
  2. s += strlen(s) advances the pointer — after each write, the pointer moves forward to the end of what was just written. There is no check that s is still within the original buffer's bounds.

Each macro call is harmless in isolation when only one or two fields are set. But when all 22 fields are populated — which is exactly what happens with a fully specified AMY event — the writes accumulate. Consider the worst case:

  • Each integer field like freq: 2147483647 contributes ~20 bytes
  • Each float field like filter_freq: 340282346638528860000000000000000000000.000 contributes up to ~50 bytes
  • 12 integer fields × 20 bytes = ~240 bytes
  • 10 float fields × 50 bytes = ~500 bytes
  • Total: ~740+ bytes, easily overflowing a 512-byte or smaller stack buffer

The Attack Scenario

An attacker controlling AMY wire protocol input can craft a message with all possible fields set to maximum values (INT32_MAX for integers, FLT_MAX for floats). When the application processes this message and calls the event-printing function, each _EPRINT_I and _EPRINT_F macro fires in sequence, advancing s past the end of the buffer. The bytes written beyond the buffer boundary overwrite adjacent stack memory — potentially including saved return addresses, function pointers, or other security-critical data.

This is not theoretical. The regression test included with the fix explicitly demonstrates that unsafe_format_event() (the old behavior) overflows a 64-byte buffer when all fields are set to 12345678 and 9876.543f, while safe_format_event() (the new behavior) correctly returns an error and leaves canary bytes intact.


The Fix

Replacing sprintf() with snprintf() Throughout

The fix makes a surgical but comprehensive change: every sprintf() call in the three macros is replaced with snprintf(), and each call receives the remaining buffer capacity as its size argument. This is computed using a base pointer s_entry that records where the buffer starts, allowing the expression len - (size_t)(s - s_entry) to give the exact number of bytes still available at each point.

// AFTER — safe
#define _EPRINT_I(FIELD, NAME, WIRECODE) \
    if (AMY_IS_SET(e->FIELD)) { \
        snprintf(s, len - (size_t)(s - s_entry), \
                 "%s%" PRId32, wirecode ? WIRECODE : " " NAME ": ", (int32_t)e->FIELD); \
        s += strlen(s); \
    }

#define _EPRINT_F(FIELD, NAME, WIRECODE) \
    if (AMY_IS_SET(e->FIELD)) { \
        snprintf(s, len - (size_t)(s - s_entry), \
                 "%s%.3f", wirecode ? WIRECODE : " " NAME ": ", e->FIELD); \
        s += strlen(s); \
    }

And for the coefficient array:

// AFTER — safe
#define _EPRINT_COEF(FIELD, NAME, WIRECODE) {
    ...
    if (last_set >= 0) {
        snprintf(s, len - (size_t)(s - s_entry), "%s", wirecode ? WIRECODE : " " NAME ": ");
        s += strlen(s);
        for (int i = 0; i <= last_set; ++i) {
            if (i > 0) {
                snprintf(s, len - (size_t)(s - s_entry), ",");
                s += strlen(s);
            }
            if (AMY_IS_SET(e->FIELD[i])) {
                snprintf(s, len - (size_t)(s - s_entry), "%.3f", e->FIELD[i]);
                s += strlen(s);
            }
        }
    }
}

Before vs. After Comparison

Aspect Before (vulnerable) After (fixed)
Write function sprintf(s, ...) snprintf(s, len - (size_t)(s - s_entry), ...)
Bounds checking None Remaining capacity enforced on every write
Buffer overrun possible Yes, with ≥22 fields set No, snprintf truncates at buffer boundary
Coefficient loop Unbounded sprintf per iteration snprintf with remaining-space tracking per iteration

Why len - (size_t)(s - s_entry) Is the Right Pattern

The key insight is that s - s_entry gives the number of bytes already written since the start of the buffer. Subtracting that from len (the total buffer size) gives the exact remaining capacity. This is computed fresh on every macro invocation, which is essential — a single check at the top of the function would not account for the space consumed by earlier fields.

This pattern is also safe from integer underflow: since s is only ever advanced forward by strlen(s) (which is non-negative), s - s_entry is always ≤ len as long as snprintf is doing its job, which it is.


Prevention & Best Practices

Never Use sprintf() in Accumulating-Pointer Patterns

Any time you see code of the form:

char buf[N];
char *s = buf;
sprintf(s, ...);
s += strlen(s);
sprintf(s, ...);   // ← danger: no remaining-space check
s += strlen(s);

treat it as a buffer overflow waiting to happen. The fix is always the same: track remaining capacity and use snprintf().

The Canonical Safe Pattern in C

char buf[N];
char *s = buf;
const char *s_entry = buf;  // base pointer, never moves
size_t remaining;

remaining = N - (size_t)(s - s_entry);
int written = snprintf(s, remaining, "field: %d", value);
if (written > 0 && (size_t)written < remaining) {
    s += written;
}

Note that using snprintf's return value directly (rather than strlen(s) after the call) is slightly more efficient and avoids a redundant string scan.

Use Compiler and Sanitizer Warnings

  • -D_FORTIFY_SOURCE=2 on GCC/Clang: enables runtime checks for certain format-string buffer overflows.
  • AddressSanitizer (-fsanitize=address): will catch out-of-bounds writes immediately in testing.
  • -Wformat-overflow (GCC 7+): warns at compile time when sprintf output may overflow a known-size buffer.
  • Static analysis: Semgrep rule c.lang.security.insecure-use-sprintf-fn and cppcheck --enable=all both flag unbounded sprintf() calls.

Relevant Standards

  • CWE-121: Stack-based Buffer Overflow — the direct classification for this vulnerability
  • CWE-787: Out-of-bounds Write — the broader category
  • CWE-676: Use of Potentially Dangerous Function — covers sprintf specifically
  • OWASP: Buffer Overflow Prevention Cheat Sheet
  • SEI CERT C: Rule STR07-C — "Use the bounds-checking interfaces for string manipulation"

Key Takeaways

  • The _EPRINT_I and _EPRINT_F macros in patches.c are called up to 22 times in sequence — any unbounded write function in a macro that runs this many times is a buffer overflow candidate, even if each individual write looks small.
  • s += strlen(s) after sprintf() is a red flag pattern — it means the code is accumulating writes with pointer arithmetic but has no mechanism to stop when the buffer is full.
  • snprintf() alone is not sufficient — you must pass the correct remaining size — passing a stale or incorrect size (e.g., always passing N instead of N - bytes_written) defeats the protection.
  • The _EPRINT_COEF loop needed fixing too — the inner for loop over coefficient array entries had the same sprintf() pattern, requiring snprintf with remaining-space tracking on every iteration, not just the first.
  • AMY wire protocol input is an attacker-controlled source — any parsing or formatting code that operates on wire data must be hardened against adversarial maximum-value inputs across all fields simultaneously.

How Orbis AppSec Detected This

  • Source: Crafted AMY wire protocol messages with all event fields set to maximum values (INT32_MAX for integers, FLT_MAX for floats), entering the event-formatting code path in src/patches.c.
  • Sink: sprintf(s, "%s%" PRId32, ...) and sprintf(s, "%s%.3f", ...) inside the _EPRINT_I and _EPRINT_F macros at src/patches.c:160, called cumulatively for up to 22 fields with no bounds checking.
  • Missing control: No size argument was passed to sprintf(), and no check was made on remaining buffer capacity before or after each macro invocation; the pointer s was advanced without limit.
  • CWE: CWE-121 — Stack-based Buffer Overflow (also CWE-787: Out-of-bounds Write; CWE-676: Use of Potentially Dangerous Function).
  • Fix: Replaced all sprintf() calls with snprintf(s, len - (size_t)(s - s_entry), ...), introducing a base pointer s_entry to compute remaining capacity on every write.

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 src/patches.c is a clear illustration of how safe-looking code becomes dangerous at scale. Each individual sprintf() write in _EPRINT_I or _EPRINT_F looks benign — it's just formatting one integer or float. But macros are called in sequence for every set field in an event, and with 22 possible fields all active, the cumulative output blows past any fixed buffer. The fix is precise and minimal: snprintf() with a dynamically computed remaining-size argument, anchored by a s_entry base pointer that never moves.

If you write C code that serializes structured data into fixed-size buffers using pointer-advancing loops or macros, audit every sprintf() call. The pattern sprintf(s, ...); s += strlen(s); repeated N times is a buffer overflow in proportion to N. Replace it with snprintf(s, remaining, ...); s += written; remaining -= written; and your buffer will never overflow regardless of how many fields are set or how large their values are.


References

Frequently Asked Questions

What is a buffer overflow in C sprintf macros?

It occurs when sprintf() writes formatted data into a buffer without checking available space. When macros accumulate many writes by advancing a pointer, the total output can exceed the buffer's allocated size, corrupting adjacent memory.

How do you prevent buffer overflow in C event-formatting code?

Replace sprintf() with snprintf() and pass the remaining buffer space as the size argument. Track remaining space by recording the base pointer at the start and computing `len - (size_t)(s - s_entry)` before each write.

What CWE is buffer overflow via sprintf?

CWE-121 (Stack-based Buffer Overflow) when the buffer is stack-allocated, and more broadly CWE-787 (Out-of-bounds Write). Using sprintf() without bounds is also flagged under CWE-676 (Use of Potentially Dangerous Function).

Is checking the buffer size once at the start enough to prevent this overflow?

No. Because each macro call advances the pointer `s`, remaining space shrinks with every field written. You must recompute remaining capacity before each individual snprintf() call, not just once at entry.

Can static analysis detect this sprintf buffer overflow pattern?

Yes. Tools like Semgrep, cppcheck, Coverity, and clang-analyzer can flag unbounded sprintf() calls. The Semgrep rule `c.lang.security.insecure-use-sprintf-fn` specifically targets this pattern.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #702

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.