Stack Overflow in C: How strcpy and strcat Put an Entire Game Suite at Risk
Introduction
There's a reason security professionals have been warning about strcpy for decades — and a recently patched vulnerability in a shared game engine header file is a perfect reminder of why that warning still matters today.
A critical-severity buffer overflow was identified and fixed in common.h, a shared header file used across an entire suite of games. The vulnerability stemmed from three unbounded string operations that blindly copied caller-supplied data into fixed-size buffers with zero length validation. Because this header is shared, a single vulnerable file put every game binary in the suite at risk simultaneously.
This is exactly the kind of vulnerability that appears in CVE databases, CTF challenges, and real-world exploits alike. Whether you're a seasoned systems programmer or just starting out with C, understanding how this class of bug works — and how to fix it — is foundational knowledge.
The Vulnerability Explained
What Is a Buffer Overflow?
A buffer overflow occurs when a program writes more data into a buffer (a fixed-size region of memory) than it was designed to hold. The excess data spills into adjacent memory, potentially overwriting critical values like saved return addresses, function pointers, or other variables.
In C, the standard library functions strcpy and strcat are notorious contributors to this class of bug because they perform no bounds checking whatsoever. They copy characters until they hit a null terminator (\0), regardless of whether the destination buffer has room.
The Vulnerable Code
The vulnerability was located at lines 41, 42, and 52 of common.h. Here's a simplified representation of what the dangerous code looked like:
// ❌ VULNERABLE CODE (before fix)
char tmp_path[256]; // Fixed-size buffer on the stack
// Line 41: Copies caller-supplied path with NO length check
strcpy(tmp_path, path);
// Line 42: Appends suffix to already potentially-overflowed buffer
strcat(tmp_path, ".XXXXXX");
// Line 52: Copies scanned name into fixed-size slot without length verification
strcpy(name_buff[location], scanned_name);
Let's break down each dangerous operation:
Problem 1: strcpy(tmp_path, path) — Line 41
tmp_path is a fixed-size buffer (e.g., 256 bytes). The path variable is caller-supplied, meaning it could be any length. If an attacker or even a legitimate user provides a path longer than 255 characters, strcpy will happily write past the end of tmp_path, corrupting whatever lives next to it in memory.
Problem 2: strcat(tmp_path, ".XXXXXX") — Line 42
Even if path just barely fits into tmp_path, appending .XXXXXX (7 more characters) could push it over the edge. This is a compound overflow — the buffer was already at risk, and this line makes it worse.
Problem 3: strcpy(name_buff[location], scanned_name) — Line 52
scanned_name comes from scanning/parsing input. If the scanned name is longer than the slot in name_buff, the overflow will corrupt adjacent entries in the buffer array or other stack/heap data nearby.
Why Is This Classified as CWE-120?
This vulnerability maps to CWE-120: Buffer Copy without Checking Size of Input — also nicknamed "Classic Buffer Overflow." It's one of the oldest and most well-documented vulnerability classes in software security, yet it continues to appear in production code.
The Multiplier Effect: A Shared Header File
What elevates this from a bad bug to a critical one is the architectural detail: common.h is a shared header included by every game in the suite. This means:
- One vulnerable file = every game binary is vulnerable
- Patching one file = every game binary is protected
- An attacker only needs to find one game with this code path exposed
This is a classic example of how shared code amplifies both risk and impact.
How Could an Attacker Exploit This?
Consider a scenario where a game reads a file path from a configuration file, user input, or a network source, then passes it to a function in common.h:
Normal path: /home/user/saves/game1 (28 chars — fits fine)
Malicious path: /home/user/saves/[300 'A' characters][shellcode address]
On a system without modern mitigations, this overflow could overwrite the saved return address on the stack. When the current function returns, instead of jumping back to legitimate code, the CPU jumps to an address controlled by the attacker — enabling arbitrary code execution.
Even with modern protections like stack canaries, ASLR, and NX bits, buffer overflows can still cause:
- Program crashes / Denial of Service
- Corruption of adjacent variables (logic bugs, privilege escalation)
- Information leaks when combined with other vulnerabilities
The Fix
The fix replaces all unbounded string operations with their length-aware, bounds-checking counterparts. Here's what the corrected code looks like:
// ✅ FIXED CODE (after patch)
#define MAX_PATH_LEN 256
#define MAX_NAME_LEN 64
char tmp_path[MAX_PATH_LEN];
// Line 41: Use strncpy — limits copy to buffer capacity
strncpy(tmp_path, path, sizeof(tmp_path) - 1);
tmp_path[sizeof(tmp_path) - 1] = '\0'; // Ensure null termination
// Line 42: Use strncat — limits append to remaining space
strncat(tmp_path, ".XXXXXX", sizeof(tmp_path) - strlen(tmp_path) - 1);
// Line 52: Validate length before copy
strncpy(name_buff[location], scanned_name, MAX_NAME_LEN - 1);
name_buff[location][MAX_NAME_LEN - 1] = '\0'; // Ensure null termination
Note: Even
strncpyhas quirks — it doesn't guarantee null termination if the source is longer thann. The explicit null termination on the line after eachstrncpycall is not optional; it's a required safety measure.
Why This Fix Works
| Before | After | Why It's Better |
|---|---|---|
strcpy(dst, src) |
strncpy(dst, src, sizeof(dst)-1) |
Limits copy to buffer size |
strcat(dst, suffix) |
strncat(dst, suffix, space_remaining) |
Limits append to available space |
| No null termination guarantee | Explicit dst[n] = '\0' |
Prevents unterminated string bugs |
The key insight is simple: every write to a fixed-size buffer must be bounded by the size of that buffer.
An Even Better Alternative: Use Safer Functions
For new code, consider using platform-specific safer alternatives:
// POSIX systems: snprintf for building strings safely
snprintf(tmp_path, sizeof(tmp_path), "%s.XXXXXX", path);
// Windows: use StringCchCopy / StringCchCat
// C11: use strcpy_s / strcat_s (Annex K)
// C++: use std::string and avoid raw buffers entirely
snprintf is particularly elegant for path construction because it handles both the copy and the append in a single, bounds-safe operation.
Prevention & Best Practices
1. Treat strcpy and strcat as Red Flags
Many organizations and style guides ban strcpy and strcat outright in new code. Configure your compiler or linter to warn on their use:
# GCC/Clang: enable fortify source
gcc -D_FORTIFY_SOURCE=2 -O2 your_code.c
# This causes runtime checks on string functions
# and compile-time warnings for detectable overflows
2. Use Static Analysis Tools
These tools can catch buffer overflow risks automatically:
- Coverity — Industry-standard static analyzer
- Clang Static Analyzer — Free, integrates with build systems
- Flawfinder — Specifically looks for dangerous C/C++ functions
- Cppcheck — Open-source C/C++ static analysis
- Semgrep — Customizable pattern-based scanning
Running any of these tools would have flagged the strcpy/strcat calls in common.h immediately.
3. Enable Compiler Hardening Flags
# Add to your Makefile or CMakeLists.txt
CFLAGS += -fstack-protector-strong # Stack canaries
CFLAGS += -D_FORTIFY_SOURCE=2 # Runtime buffer checks
CFLAGS += -Wformat -Wformat-security # Format string warnings
LDFLAGS += -Wl,-z,relro,-z,now # RELRO protection
These don't replace fixing the bug, but they add layers of defense that make exploitation harder.
4. Apply the Principle of Input Validation
Any time your code accepts external input (file paths, usernames, network data), validate it before using it:
// Always validate input length before processing
if (strlen(path) >= MAX_PATH_LEN) {
fprintf(stderr, "Error: path too long\n");
return ERROR_INVALID_INPUT;
}
// Now safe to proceed
strncpy(tmp_path, path, sizeof(tmp_path) - 1);
5. Consider Moving to Memory-Safe Languages
For new projects, languages like Rust, Go, and modern C++ with smart pointers eliminate entire classes of memory safety bugs by design. The irony in this case? The project already had Rust in its dependency tree (via src-tauri/Cargo.lock) — Rust's borrow checker and string types would have made this vulnerability impossible in idiomatic code.
6. Know the Relevant Standards
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-121: Stack-based Buffer Overflow
- OWASP: Buffer Overflow
- CERT C Coding Standard: STR31-C: Guarantee sufficient space for string storage
Key Takeaways
This vulnerability is a textbook example of why C string handling demands constant vigilance:
-
strcpyandstrcatare dangerous — they have no awareness of destination buffer size and should be replaced with bounds-checking alternatives in all new and maintained code. -
Shared code multiplies risk — a single vulnerable header file put an entire game suite at risk. Audit your shared libraries and headers with extra scrutiny.
-
The fix is straightforward — replacing unbounded operations with
strncpy/strncat/snprintfand explicitly null-terminating buffers closes the vulnerability cleanly. -
Defense in depth matters — compiler hardening flags, static analysis, and input validation all work together to reduce the attack surface.
-
Automated scanning works — this vulnerability was caught by an automated multi-agent AI scanner, demonstrating the value of integrating security tooling into your CI/CD pipeline.
Buffer overflows have been in the OWASP Top 10 and CWE Top 25 for years — not because they're exotic, but because they keep appearing in production code. The best time to fix a buffer overflow is before it ships. The second best time is right now.
This post was generated as part of an automated security fix workflow by OrbisAI Security. Vulnerability ID: V-001 | Severity: Critical | CWE-120.