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

high

How buffer overflow via insecure strcpy/strncpy happens in C textbox widgets and how to fix it

A high-severity buffer overflow vulnerability was discovered in the Aroma UI framework's textbox widget where `strncpy()` was used to copy user-provided text without guaranteed null-termination safety. The fix replaces the dangerous `strncpy()` pattern with `snprintf()`, which automatically handles buffer boundaries and null-termination in a single, safer operation.

critical

How buffer overflow via sprintf happens in C++ fuzzer code and how to fix it

A critical buffer overflow vulnerability was discovered in `prog/fuzzing/recog_basic_fuzzer.cc` where `sprintf` writes to a fixed 256-byte buffer without bounds checking. An attacker providing crafted fuzzer input could exploit this to corrupt memory. The fix replaces `sprintf` with `snprintf`, enforcing the buffer size limit and preventing overflow.

critical

How buffer overflow in memcpy happens in C bios_disk.h and how to fix it

A critical buffer overflow vulnerability was discovered in `include/bios_disk.h` at line 474, where a `memcpy` operation copies 512 bytes from a source buffer without properly validating that the calculated offset from the `sectnum` parameter stays within bounds. An attacker controlling the `sectnum` parameter could trigger an out-of-bounds read, potentially leaking sensitive memory contents or causing a crash. The fix adds a proper bounds check before the memcpy call to ensure the source offset

critical

How buffer overflow happens in C RTSPSession.h and how to fix it

A critical buffer overflow vulnerability in `src/AudioTools/Communication/RTSP/RTSPSession.h` allowed an attacker to send a crafted RTSP request with an oversized payload, triggering a heap overflow via an unchecked `memcpy()` call at line 408. The fix adds a single bounds check before the copy and replaces several unsafe `strcpy`/`strncpy` calls with `snprintf`, closing multiple paths to memory corruption and potential remote code execution.

medium

How buffer overflow happens in C kernel driver (qcom_usbnet_main.c) and how to fix it

A series of unsafe `sprintf()` calls in the Qualcomm USB network kernel driver (`qcom_usbnet_main.c`) created buffer overflow conditions that, when combined with other memory corruption primitives in the same file, could allow an attacker with physical USB access to escalate privileges to root. The fix replaces unbounded `sprintf()` and `snprintf()` misuse with properly bounded `snprintf()` and `scnprintf()` calls that respect actual buffer sizes. This is a textbook example of how a seemingly mi

critical

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in `script/llm_semantic_analyzer.py` at line 394, where user-controlled input (API keys and model parameters) was interpolated directly into shell commands passed to `subprocess.run` with `shell=True`. An attacker who could control these parameters could inject shell metacharacters like `; rm -rf /` or `$(whoami)` to execute arbitrary commands. The fix sanitizes all user input before it reaches shell execution.