Back to Blog
critical SEVERITY7 min read

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

A critical buffer overflow vulnerability was discovered in `mupen64plus-rsp-cxd4/module.c`, where unsafe `sprintf()` and `strcpy()` calls at lines 294–298 could be exploited by a crafted N64 ROM file to corrupt memory and achieve code execution. The fix replaces these unbounded string functions with `snprintf()`, which enforces strict buffer size limits and eliminates the overflow risk. This is a textbook example of how legacy C string functions can silently introduce critical security flaws in

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

Answer Summary

This is a critical stack buffer overflow vulnerability (CWE-120) in C, found in `mupen64plus-rsp-cxd4/module.c` at lines 294–298. The root cause is the use of unbounded `sprintf()` and `strcpy()` functions to write attacker-influenced data into fixed-size stack buffers — a pattern that allows a crafted N64 ROM to overflow the buffer and corrupt memory. The fix replaces all three unsafe calls with `snprintf()`, passing `sizeof(buffer)` as the size limit to prevent any write beyond the buffer boundary. Combined with related out-of-bounds write and heap corruption issues in the same codebase, this formed a realistic code execution attack chain triggered simply by opening a malicious ROM file.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf()/strcpy() with snprintf() using sizeof(buffer) as the size limit
riskArbitrary code execution triggered by opening a malicious N64 ROM file
languageC
root causeUnbounded sprintf() and strcpy() write attacker-influenced data into fixed-size stack buffers
vulnerabilityStack Buffer Overflow via sprintf/strcpy

How Buffer Overflow Happens in C sprintf/strcpy and How to Fix It

Introduction

The mupen64plus-rsp-cxd4/module.c file is the heart of a Reality Signal Processor (RSP) plugin for the mupen64plus N64 emulator. Its step_SP_commands() function processes RSP microcode instructions — decoding them, formatting their hex representations, and logging diagnostic output. Sounds benign. But buried inside that logging logic, three consecutive unsafe string calls created a critical memory safety vulnerability that could let an attacker execute arbitrary code just by convincing a user to open a crafted ROM file.

The specific offenders were sprintf() at line 294, sprintf() again at line 295, and strcpy() at line 296 — all writing into fixed-size stack buffers with no bounds checking whatsoever.

The Vulnerability Explained

Here is the vulnerable code as it existed before the fix:

// mupen64plus-rsp-cxd4/module.c, lines 291–298 (BEFORE)
endian_swap[01] = (u8)((inst >> 16) & 0xFF);
endian_swap[02] = (u8)((inst >>  8) & 0xFF);
endian_swap[03] = (u8)((inst >>  0) & 0xFF);
sprintf(&offset[0], "%03X", GET_RCP_REG(SP_PC_REG) & 0xFFF);
sprintf(&code[0], "%08X", inst);
strcpy(text, offset);
my_strcat(text, "\n");
my_strcat(text, code);
message(text); /* PC offset, MIPS hex. */

Three problems exist here simultaneously:

Problem 1: sprintf() into offset[]

sprintf(&offset[0], "%03X", GET_RCP_REG(SP_PC_REG) & 0xFFF) writes a hex-formatted program counter value into offset. The %03X format specifier suggests 3 characters, but sprintf() does not enforce this — if the format string or the value ever changes, or if the buffer is smaller than assumed, there is no safety net.

Problem 2: sprintf() into code[]

Similarly, sprintf(&code[0], "%08X", inst) writes an 8-character hex representation of the instruction word. Again, no size bound is passed to the function.

Problem 3: strcpy(text, offset) — the most dangerous call

strcpy() copies bytes from offset into text until it hits a null terminator. It has absolutely no knowledge of how large text is. If offset contains more data than text can hold, the overflow silently corrupts the stack.

Why This Is Exploitable

The inst parameter to step_SP_commands() ultimately derives from RSP microcode loaded from an N64 ROM. A maliciously crafted ROM can supply arbitrary instruction words. While the & 0xFFF mask on the PC register limits that specific value, the broader attack surface is clear: the code assumes fixed output sizes that are never actually enforced.

More critically, this vulnerability is part of a combined attack chain identified in the same PR:

  1. Stack buffer overflow via sprintf/strcpy in module.c:294–298
  2. Out-of-bounds writes in game_controller.c:64/77 via malformed SI commands
  3. Heap corruption via double-free/mismatched allocator in module.c:390–396 on Windows

Any single one of these paths can corrupt memory. Together, they form a realistic code execution exploit. The attack vector requires no network access, no authentication, no elevated privileges — just opening a .z64 file.

CWE-120 — "Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')" — applies directly here. The CVSS profile for this kind of local file-triggered code execution in a widely-used emulator plugin is critical.

The Fix

The fix is surgical and correct: every unsafe string function is replaced with snprintf(), and the buffer size is passed explicitly using sizeof():

// mupen64plus-rsp-cxd4/module.c, lines 291–298 (AFTER)
endian_swap[01] = (u8)((inst >> 16) & 0xFF);
endian_swap[02] = (u8)((inst >>  8) & 0xFF);
endian_swap[03] = (u8)((inst >>  0) & 0xFF);
snprintf(&offset[0], sizeof(offset), "%03X", GET_RCP_REG(SP_PC_REG) & 0xFFF);
snprintf(&code[0], sizeof(code), "%08X", inst);
snprintf(text, sizeof(text), "%s", offset);
my_strcat(text, "\n");
my_strcat(text, code);
message(text); /* PC offset, MIPS hex. */

Before vs. After

Location Before After
Line 294 sprintf(&offset[0], "%03X", ...) snprintf(&offset[0], sizeof(offset), "%03X", ...)
Line 295 sprintf(&code[0], "%08X", inst) snprintf(&code[0], sizeof(code), "%08X", inst)
Line 296 strcpy(text, offset) snprintf(text, sizeof(text), "%s", offset)

Why Each Change Matters

snprintf() vs sprintf(): snprintf() accepts a maximum number of bytes to write (including the null terminator). It will never write more than sizeof(buffer) bytes, regardless of what the format string or input data produces. The output is always null-terminated within the buffer boundary.

snprintf(text, sizeof(text), "%s", offset) vs strcpy(text, offset): This is the most important substitution. strcpy() is inherently unsafe — it will copy until it finds \0, with no regard for the destination size. Replacing it with snprintf() using sizeof(text) as the limit means even if offset is somehow longer than expected, text cannot overflow. The "%s" format also avoids a format string injection risk that a direct snprintf(text, sizeof(text), offset) would introduce.

Using sizeof(buffer) rather than a hardcoded constant: This is the right pattern because it ties the size limit directly to the actual allocation. If the buffer size is ever changed during refactoring, the sizeof() expression automatically reflects the new size — no manual constant to update and potentially get wrong.

Prevention & Best Practices

1. Ban sprintf(), strcpy(), and strcat() in new C code

These functions should be treated as deprecated in security-sensitive code. Modern replacements exist:

Unsafe Safe Replacement
sprintf(buf, ...) snprintf(buf, sizeof(buf), ...)
strcpy(dst, src) snprintf(dst, sizeof(dst), "%s", src) or strlcpy()
strcat(dst, src) strncat() or snprintf() with remaining space
gets() fgets()

2. Use compiler warnings and hardening flags

# Enable warnings that catch unsafe string use
gcc -Wall -Wformat -Wformat-overflow -Wformat-truncation

# Enable AddressSanitizer during development/testing
gcc -fsanitize=address,undefined

3. Enable static analysis in CI/CD

Tools that would catch this pattern:
- Semgrep: Rules for sprintf and strcpy usage in C
- Coverity / CodeQL: Dataflow analysis that traces tainted input to unsafe sinks
- Clang Static Analyzer: Built-in checks for buffer overflow patterns
- Orbis AppSec: Automatically detected and patched this exact vulnerability

4. Follow OWASP and CWE guidance

  • CWE-120: Avoid unbounded buffer copy operations
  • CWE-121: Stack-based buffer overflows are especially dangerous because they can overwrite return addresses
  • OWASP Input Validation Cheat Sheet: Validate and bound all input before processing

5. Audit the full call chain, not just the obvious sink

This vulnerability was part of a three-way attack chain. When auditing C code for buffer safety, trace the full data flow from ROM/file input through every intermediate buffer — not just the final output call.

Key Takeaways

  • strcpy(text, offset) in step_SP_commands() is the highest-risk call — it copies an unbounded string into a stack buffer with zero size enforcement, making it the most direct path to stack corruption.
  • sprintf() without a size limit is never safe, even when the format string looks like it constrains output — format strings are not a substitute for actual buffer size parameters.
  • Using sizeof(buffer) in snprintf() is safer than hardcoded constants — it automatically adapts if the buffer declaration changes during maintenance.
  • Emulator plugins are high-value targets because they process untrusted, attacker-controlled file content (ROM data) with the full privileges of the host process.
  • A single unsafe string function in a logging path can anchor a full code execution chain — this vulnerability combined with two others in the same codebase to form a realistic exploit requiring only a malicious .z64 file.

How Orbis AppSec Detected This

  • Source: Attacker-controlled RSP instruction data loaded from a crafted N64 ROM file, passed as the inst parameter to step_SP_commands(uint32_t inst)
  • Sink: strcpy(text, offset) and sprintf(&offset[0], "%03X", ...) in mupen64plus-rsp-cxd4/module.c:294–296 — unbounded writes into fixed-size stack buffers
  • Missing control: No buffer size limit was passed to sprintf() or strcpy(), allowing writes beyond the allocated stack buffer boundaries
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
  • Fix: All three unsafe calls were replaced with snprintf() with explicit sizeof(buffer) size limits, preventing any write beyond the allocated buffer

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 step_SP_commands() function in mupen64plus-rsp-cxd4/module.c illustrates a pattern that appears constantly in legacy C codebases: diagnostic and logging code written quickly, without the same security scrutiny applied to core logic. Three lines of formatting code — sprintf, sprintf, strcpy — created a critical vulnerability that could be triggered by any user who opens a malicious ROM file.

The fix is three lines long and follows a principle every C developer should internalize: never use a string copy or format function that doesn't accept a destination size. snprintf() with sizeof(buffer) is not just a drop-in replacement — it's a fundamentally safer API that eliminates an entire class of memory corruption bugs.

If you maintain C code that handles untrusted input (files, network data, user input), audit every sprintf(), strcpy(), and strcat() call in your codebase today. The effort is small. The alternative — a working code execution exploit from a single file open — is not.

References

Frequently Asked Questions

What is a stack buffer overflow?

A stack buffer overflow occurs when a program writes more data into a fixed-size stack-allocated buffer than it can hold, overwriting adjacent memory — including return addresses — which can allow an attacker to hijack program execution.

How do you prevent buffer overflows in C?

Always use size-bounded string functions like snprintf(), strlcpy(), or strncpy() instead of sprintf(), strcpy(), or strcat(). Pass the exact buffer size as a parameter so the function cannot write beyond the allocation.

What CWE is a buffer overflow?

Classic buffer overflow is classified as CWE-120 ("Buffer Copy without Checking Size of Input"). Related CWEs include CWE-121 (Stack-based Buffer Overflow) and CWE-122 (Heap-based Buffer Overflow).

Is input validation enough to prevent buffer overflows in C?

Input validation helps but is not sufficient on its own. The underlying string functions must also enforce size limits. A single missed validation path can still lead to exploitation, so defense-in-depth — validating input AND using safe APIs — is required.

Can static analysis detect buffer overflows?

Yes. Tools like Semgrep, Coverity, CodeQL, and AddressSanitizer (ASan) can detect unsafe uses of sprintf() and strcpy() at compile time or runtime. Orbis AppSec's multi-agent AI scanner detected this exact pattern automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #814

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.