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

high

How insecure string copy functions happen in C calculations.c and how to fix it

A high-severity buffer overflow vulnerability was discovered in `src/calculations.c` at line 37, where a two-step `strncpy` + manual null-termination pattern left the door open for subtle memory safety bugs when copying string data into the `entry->type` field. The fix replaces both lines with a single `snprintf` call that handles bounds and null-termination atomically, eliminating the risk entirely. This is a common C pitfall that affects production CLI tools and can be exploited when attacker-

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How integer overflow in buffer size calculation happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).

critical

How integer overflow happens in C reliable.c and how to fix it

A critical integer overflow vulnerability was discovered in `reliable.c` at line 1299, where the `packet_buffer_size` calculation used signed `int` arithmetic that could wrap to a negative or undersized value when large `fragment_size` values were involved. By casting each operand to `size_t` before multiplication, the fix eliminates the overflow risk entirely and ensures the allocated buffer is always large enough to hold the reassembled packet data.

high

How insecure string copy functions happen in C (cyw43.c) and how to fix it

Three unsafe string copy calls in `src/cyw43.c` — including a bare `strcpy()` and two `strncpy()` calls — created buffer overflow risks in a CYW43 Wi-Fi driver emulation layer. The fix replaces all three with `snprintf()`, which enforces buffer size limits and guarantees null-termination in a single, consistent operation. Left unaddressed, these vulnerabilities could allow an attacker controlling input like a TAP interface name or SSID to corrupt adjacent memory and potentially execute arbitrary