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++ and how to fix it

A critical buffer overflow in `create_hex_string()` within `hmlangw.cpp` let an unconditional 16-iteration loop write past the bounds of a 100-byte `hex` buffer using unchecked `sprintf` calls. The fix replaces `sprintf` with `snprintf` and caps the loop iterations based on the actual destination buffer size, closing off a memory corruption path reachable from serial or network input.

high

How insecure-use-string-copy-fn happens in C and how to fix it

A high-severity vulnerability was identified in `plugin/bin/install.c` where `strcpy()` and `strncpy()` were used to handle path strings without proper bounds checking or guaranteed null-termination. The fix replaces `strcpy()` with direct character assignment and `strncpy()` with `snprintf()`, eliminating both buffer overflow and missing null-terminator risks in the plugin installation workflow.

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.