Back to Blog
medium SEVERITY8 min read

How stack buffer overflow happens in C PMenu_Do_Update() and how to fix it

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

O
By Orbis AppSec
Published July 5, 2026Reviewed July 5, 2026

Answer Summary

This is a stack buffer overflow (CWE-120) in the C function `PMenu_Do_Update()` in `src/menu/menu.c`. The vulnerability arises from repeated `sprintf()` calls that accumulate formatted strings into a fixed 1400-byte stack buffer without tracking how much space remains. An attacker who can control player names or menu entry text can overflow the buffer, potentially corrupting the stack. The fix replaces all `sprintf()` calls with `snprintf()` using a tracked `len` variable to enforce the buffer boundary on every write.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf() with snprintf() using a size_t len counter to bound every write
riskStack corruption, potential code execution or crash via long menu entry strings
languageC
root causeRepeated sprintf() calls into a fixed 1400-byte buffer with no remaining-space tracking
vulnerabilityStack Buffer Overflow

How Stack Buffer Overflow Happens in C PMenu_Do_Update() and How to Fix It

Summary

A stack buffer overflow in PMenu_Do_Update() within src/menu/menu.c allowed repeated unchecked sprintf() calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every sprintf(string + strlen(string), ...) call with snprintf(string + len, sizeof(string) - len, ...), tracking remaining buffer space and preventing writes beyond the buffer boundary.


Introduction

The src/menu/menu.c file handles in-game player menus — team selection, inventory display, and similar UI elements. Its PMenu_Do_Update() function is responsible for building the layout string that gets sent to the client to render the menu. But a subtle, dangerous pattern lurked inside: the function accumulated formatted strings into a single 1400-byte stack buffer using repeated sprintf() calls, with no tracking of how much space remained after each write.

This is a textbook instance of CWE-120 — a classic buffer overflow — and it's particularly dangerous because the overflow target is on the stack. An attacker who can control the text of a menu entry (for example, by setting a long player name that appears in a team selection menu) could overflow the buffer, corrupt adjacent stack memory, and potentially hijack control flow.


The Vulnerability Explained

The Dangerous Pattern in PMenu_Do_Update()

Inside PMenu_Do_Update(), a 1400-byte stack buffer named string is declared, and the function builds a UI layout string by appending formatted chunks to it in a loop:

// VULNERABLE CODE — before the fix
strcpy(string, "xv 32 yv 8 picn inventory ");

for (i = 0, p = hnd->entries; i < hnd->num; i++, p++)
{
    // ... text processing ...

    sprintf(string + strlen(string), "yv %d ", 32 + i * 8);

    // ... alignment logic ...

    sprintf(string + strlen(string), "xv %d ", x - ((hnd->cur == i) ? 8 : 0));

    if (hnd->cur == i)
    {
        sprintf(string + strlen(string), "string2 \"\x0d%s\" ", t);
    }
    else if (alt)
    {
        sprintf(string + strlen(string), "string2 \"%s\" ", t);
    }
    else
    {
        sprintf(string + strlen(string), "string \"%s\" ", t);
    }
}

There are several compounding problems here:

  1. sprintf() has no size limit. It will write as many bytes as the format string and arguments produce, regardless of how much space remains in string.
  2. strlen(string) is used to find the "end" of the buffer, but this only tells you where to start writing — not how many bytes are left before the buffer ends.
  3. The loop runs once per menu entry. Each iteration appends multiple formatted strings. With enough entries, or with sufficiently long entry text (like a player name), the total easily exceeds 1400 bytes.
  4. The buffer is on the stack. Overflowing it overwrites the function's return address and other local variables, which is the classic setup for a stack smashing attack.

The Attack Scenario

Consider a team selection menu that displays player names. If an attacker sets their player name to a 200-character string, each iteration of the loop appends something like:

string "AAAAAAAAAA...AAA (200 chars)" 

That's roughly 210 bytes per menu entry. With just 7 players in the menu, the buffer is already at ~1470 bytes — past the 1400-byte boundary. The overflow begins writing into adjacent stack memory, corrupting the saved frame pointer and return address of PMenu_Do_Update().

When the function returns, execution jumps to attacker-controlled data. On modern systems with stack canaries and ASLR this is harder to exploit reliably, but it still causes crashes (denial of service) and may be exploitable in environments without those mitigations.


The Fix

The fix introduces a single size_t len variable to track how many bytes have been written so far, and replaces every sprintf() call with snprintf() that passes sizeof(string) - len as the size limit.

Before and After

Before (line 175, vulnerable):

strcpy(string, "xv 32 yv 8 picn inventory ");

After (safe):

len = snprintf(string, sizeof(string), "xv 32 yv 8 picn inventory ");

Before (line 192, vulnerable):

sprintf(string + strlen(string), "yv %d ", 32 + i * 8);

After (safe):

len += snprintf(string + len, sizeof(string) - len, "yv %d ", 32 + i * 8);

Before (lines 210, 214, 218, vulnerable):

sprintf(string + strlen(string), "string2 \"\x0d%s\" ", t);
sprintf(string + strlen(string), "string2 \"%s\" ", t);
sprintf(string + strlen(string), "string \"%s\" ", t);

After (safe):

len += snprintf(string + len, sizeof(string) - len, "string2 \"\x0d%s\" ", t);
len += snprintf(string + len, sizeof(string) - len, "string2 \"%s\" ", t);
len += snprintf(string + len, sizeof(string) - len, "string \"%s\" ", t);

Why This Works

snprintf(dest, n, fmt, ...) guarantees it will write at most n - 1 bytes plus a null terminator. By passing sizeof(string) - len as n, each call knows exactly how many bytes remain in the buffer. Once len reaches or exceeds sizeof(string), subsequent calls receive 0 or a very small n and write nothing (or just a null terminator), safely truncating the output instead of overflowing.

The len variable accumulates the total written bytes, making strlen(string) calls unnecessary — this also eliminates the O(n²) behavior of the original loop, which called strlen() on an ever-growing string on every iteration.

The Additional Fix in PMenu_Open()

The diff also hardens PMenu_Open():

// BEFORE
hnd->entries = malloc(sizeof(pmenu_t) * num);
memcpy(hnd->entries, entries, sizeof(pmenu_t) * num);

// AFTER
if (num <= 0)
{
    free(hnd);
    return NULL;
}
hnd->entries = calloc((size_t)num, sizeof(pmenu_t));
memcpy(hnd->entries, entries, sizeof(pmenu_t) * (size_t)num);

This adds a guard against zero or negative num values (which would cause malloc(0) or a large allocation due to integer wrap), and switches to calloc() which zero-initializes memory, preventing use of uninitialized data.


Prevention & Best Practices

1. Never Use sprintf() for Buffer Accumulation

sprintf() is inherently unsafe when writing into a fixed-size buffer. Always use snprintf() and always pass the remaining buffer size, not the total size:

// Wrong — passes total size, ignores already-written bytes
snprintf(buf + offset, sizeof(buf), ...);

// Correct — passes remaining size
snprintf(buf + offset, sizeof(buf) - offset, ...);

2. Track Written Bytes, Not String Length

Using strlen(buf) to find the write position is O(n) and doesn't tell you the remaining space. Maintain a len or offset variable from the start:

size_t len = 0;
len += snprintf(buf + len, sizeof(buf) - len, "part1 %s", s1);
len += snprintf(buf + len, sizeof(buf) - len, "part2 %d", n);

3. Check snprintf() Return Values for Truncation

snprintf() returns the number of bytes that would have been written if the buffer were large enough. If the return value equals or exceeds the size limit, truncation occurred:

int written = snprintf(buf + len, sizeof(buf) - len, fmt, arg);
if (written < 0 || (size_t)written >= sizeof(buf) - len) {
    // Handle truncation — log, error out, or resize buffer
}
len += (size_t)written;

4. Use Compiler and Linker Hardening

Enable stack protection features during compilation:
- -fstack-protector-strong — adds stack canaries to detect overflow before return
- -D_FORTIFY_SOURCE=2 — enables compile-time and runtime checks on sprintf, strcpy, etc.
- -Wformat-overflow — GCC warning for statically detectable format string overflows

5. Static Analysis Tools

  • Semgrep: Rules for sprintf into fixed buffers — https://semgrep.dev/r?q=sprintf
  • Clang Static Analyzer: Detects buffer overflows in C/C++
  • Coverity: Enterprise-grade detection of CWE-120 patterns
  • AddressSanitizer (ASan): Runtime detection — compile with -fsanitize=address

Key Takeaways

  • sprintf(buf + strlen(buf), ...) is a red flag in any C codebase. It tells you the developer is accumulating into a buffer but not tracking remaining space. Search your codebase for this pattern.
  • In PMenu_Do_Update(), the overflow was loop-amplified. Each menu entry added multiple sprintf() calls, meaning the overflow risk scaled with the number of entries and the length of player names — both attacker-influenced values.
  • Switching to snprintf() alone isn't enough. You must pass sizeof(buffer) - current_offset as the size, not sizeof(buffer). The fix correctly uses sizeof(string) - len on every call.
  • calloc() over malloc() for entry arrays. The PMenu_Open() fix also switched to calloc(), ensuring zero-initialized memory and guarding against use of uninitialized pointer fields in menu entries.
  • The num <= 0 guard in PMenu_Open() closes a related integer handling bug. Without it, a zero or negative entry count could produce a zero-size allocation or integer overflow in the sizeof(pmenu_t) * num multiplication.

How Orbis AppSec Detected This

  • Source: Menu entry text strings (p->text) populated from player-controlled data such as player names in team selection menus
  • Sink: sprintf(string + strlen(string), "string \"%s\" ", t) at src/menu/menu.c:218 (and related calls at lines 172, 192, 210, 214)
  • Missing control: No remaining-buffer-size tracking; sprintf() used instead of snprintf(), allowing unbounded writes into the 1400-byte stack buffer string
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • Fix: Introduced size_t len to track bytes written and replaced all sprintf() calls with snprintf(string + len, sizeof(string) - len, ...) to enforce the buffer boundary

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 PMenu_Do_Update() is a clear example of how a common, decades-old C pattern — appending to a string buffer with sprintf() — becomes dangerous the moment user-controlled data enters the picture. The fix is straightforward: one size_t len variable and a consistent switch to snprintf() with sizeof(string) - len. The cost of the fix is minimal; the cost of leaving it unfixed could be a crashed server or, in a less hardened environment, arbitrary code execution triggered by a long player name.

When writing C code that builds strings incrementally, always ask: do I know exactly how many bytes remain in this buffer at every write? If the answer is no, it's time to introduce a length tracker and switch to snprintf().


References

Frequently Asked Questions

What is a stack buffer overflow?

A stack buffer overflow occurs when a program writes more data into a stack-allocated buffer than it can hold, overwriting adjacent memory such as return addresses or local variables, potentially leading to crashes or code execution.

How do you prevent stack buffer overflow in C?

Use snprintf() instead of sprintf(), always pass the remaining buffer size as the limit, and track how many bytes have been written with a length counter so each subsequent write knows exactly how much space is left.

What CWE is stack buffer overflow?

Stack buffer overflows are classified under CWE-120 (Buffer Copy without Checking Size of Input, also known as "Classic Buffer Overflow").

Is bounds checking on the input string enough to prevent this overflow?

Not on its own. Even if individual strings are short, accumulating many formatted writes into one buffer without tracking remaining space can still overflow it. You must track the running length and pass the remaining size to every snprintf() call.

Can static analysis detect stack buffer overflow from sprintf?

Yes. Tools like Semgrep, Coverity, and clang-tidy can flag sprintf() calls into fixed-size buffers. Orbis AppSec's multi-agent AI scanner detected this exact pattern in menu.c and opened a fix PR automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #30

Related Articles

critical

How heap buffer overflow happens in C dupstr() and how to fix it

A critical heap buffer overflow vulnerability was discovered in the `dupstr()` function inside `tools/strliteral.c`, where `strcpy()` was called on a heap-allocated buffer without first verifying that `malloc()` had succeeded. If `malloc()` returned `NULL`, the subsequent `strcpy()` would write into address zero — corrupting memory and potentially enabling arbitrary code execution. The fix replaces the unsafe `strcpy()` call with a `NULL` check followed by a bounds-safe `memcpy()`, closing the v

critical

How buffer overflow happens in C TinyGSM sprintf and how to fix it

A critical buffer overflow vulnerability was discovered in `TinyGsmClientSequansMonarch.h` at line 515, where `sprintf` was writing a two-character hex string into a buffer only two bytes large — leaving no room for the null terminator. The fix replaces `sprintf` with `snprintf` and increases the buffer to three bytes, preventing a one-byte overflow that could corrupt adjacent memory in embedded firmware.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.

critical

How buffer overflow in strcpy() happens in C configuration parsing and how to fix it

A critical buffer overflow vulnerability in `src/rpconfig.h` allowed attackers to corrupt memory by providing configuration values exceeding buffer size limits. The `rpcSetText()` function used `strcpy()` to copy user-controlled data into a fixed 256-byte buffer without bounds checking, enabling stack corruption and potential code execution. Replacing `strcpy()` with `strncpy()` and enforcing a 255-byte limit eliminated the overflow risk.

critical

How buffer overflow happens in C gdb-server and how to fix it

A critical buffer overflow vulnerability was discovered in `src/st-util/gdb-server.c` where unbounded `memcpy()` and `strcpy()` calls could write beyond allocated buffer boundaries when processing user-supplied command-line arguments. The fix replaces all unsafe string operations with bounds-checked alternatives like `snprintf()` and `memcpy()` with explicit length validation.

medium

How integer overflow in bounds checking happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the W_Read function of DOOM/w_file.c that allowed attackers to bypass bounds checking by crafting WAD files with malicious offset values near UINT_MAX. The fix implements a two-step validation approach that first checks if the offset exceeds the file length, then safely calculates the remaining bytes without risk of overflow.