Back to Blog
critical SEVERITY9 min read

Stack Buffer Overflow in tpl.c: How strcpy Without Bounds Checking Enables Full Control Flow Hijacking

A critical stack buffer overflow vulnerability was discovered and patched in tpl.c, where command-line arguments were copied into fixed-size stack buffers using strcpy without any length validation. An attacker supplying an oversized argument could overwrite the saved return address on the stack, achieving complete control flow hijacking. The fix eliminates this classic but devastatingly effective vulnerability class that has plagued C programs for decades.

O
By Orbis AppSec
Published May 14, 2026Reviewed June 3, 2026

Answer Summary

A stack buffer overflow (CWE-121) was found in tpl.c, a C program that used strcpy() to copy command-line arguments into fixed-size stack buffers without length validation. This allows attackers to overwrite the return address and hijack control flow. The fix replaces strcpy() with strncpy() or snprintf() with explicit size bounds matching the destination buffer, preventing overflow regardless of input length.

Vulnerability at a Glance

cweCWE-121
fixReplace strcpy() with strncpy() or snprintf() using the destination buffer size as the length limit
riskComplete control flow hijacking, arbitrary code execution
languageC
root causeUsing strcpy() to copy user-controlled command-line arguments into fixed-size stack buffers without length validation
vulnerabilityStack Buffer Overflow via strcpy()

Stack Buffer Overflow in tpl.c: How strcpy Without Bounds Checking Enables Full Control Flow Hijacking

Severity: 🔴 CRITICAL | CVE Class: CWE-121 (Stack-based Buffer Overflow) | Fixed in: tpl.c


Introduction

Few vulnerability classes have caused as much damage over the decades as the humble stack buffer overflow. From the Morris Worm of 1988 to modern exploit chains targeting critical infrastructure, the pattern remains stubbornly the same: a developer trusts user-supplied input, copies it into a fixed-size buffer without checking its length, and hands an attacker the keys to the kingdom.

This week, we're examining a critical stack buffer overflow discovered and patched in tpl.c — a textbook example of why strcpy is considered one of the most dangerous functions in the C standard library. If you write C or C++ code, or if you maintain software that does, this post is essential reading.


The Vulnerability Explained

What Happened?

Inside tpl.c, the code processes command-line arguments and copies them into fixed-size stack-allocated buffers. The problem is deceptively simple:

// VULNERABLE CODE (simplified representation)
char path[256];   // Fixed-size buffer on the stack
char cmd[256];    // Another fixed-size buffer on the stack

// Line 61: copies argv[i]+5 directly into 'path' — no length check!
strcpy(path, argv[i] + 5);

// Lines 67-68: copies argv[0] into 'cmd', then appends '.exe'
strcpy(cmd, argv[0]);
strcat(cmd, ".exe");

The strcpy function copies bytes from the source string to the destination buffer and stops only when it encounters a null terminator (\0). It has absolutely no concept of the destination buffer's size. If the source string is longer than the destination buffer, strcpy will happily keep writing — right past the end of the buffer and into adjacent stack memory.

Understanding the Stack Layout

To appreciate why this is so dangerous, you need to understand how the stack is organized at runtime:

Higher memory addresses
┌─────────────────────────┐
   ... other data ...    
├─────────────────────────┤
   Saved Return Address     Where execution jumps after function returns
├─────────────────────────┤
   Saved Base Pointer    
├─────────────────────────┤
   char cmd[256]            Buffer #2
├─────────────────────────┤
   char path[256]           Buffer #1 — strcpy writes here
├─────────────────────────┤
   ... local variables   
└─────────────────────────┘
Lower memory addresses (stack grows downward)

When strcpy writes more than 256 bytes into path, it overflows upward through cmd, through the saved base pointer, and ultimately overwrites the saved return address. When the function returns, instead of jumping back to legitimate code, the CPU jumps to wherever the attacker told it to go.

How Could It Be Exploited?

The attack is straightforward for anyone with basic exploit development knowledge:

Step 1 — Reconnaissance: Identify that the program accepts command-line arguments and passes them to strcpy.

Step 2 — Craft the payload: Create an argument longer than 256 characters. The first 256+ bytes fill and overflow the buffer. The bytes at the precise offset of the saved return address are replaced with the attacker's chosen address.

Step 3 — Redirect execution: The attacker's chosen address might point to:
- Shellcode injected earlier in the oversized buffer itself
- A ROP (Return-Oriented Programming) chain that chains together existing code snippets to bypass modern mitigations like NX/DEP
- A system function like system() with attacker-controlled arguments (ret2libc attack)

# Simplified attack concept — oversized argument triggers overflow
./tpl --path=$(python3 -c "print('A' * 300 + '\xef\xbe\xad\xde')")
#                                  ^fills buffer  ^overwrites return addr

Step 4 — Achieve arbitrary code execution: With control over the instruction pointer (RIP/EIP), the attacker can execute arbitrary code with the privileges of the running process.

Real-World Impact

The impact of successful exploitation is severe:

Impact Category Description
Confidentiality Full read access to process memory, credentials, secrets
Integrity Arbitrary code execution — attacker can modify any data
Availability Process crash (even a "failed" exploit is a DoS)
Privilege Escalation If the binary runs as root or with elevated privileges, full system compromise

Even in environments with modern mitigations (ASLR, stack canaries, NX), a determined attacker with sufficient information leaks can often bypass these protections. The vulnerability should be treated as directly exploitable.


The Fix

What Changes Were Made?

The fix addresses the root cause: replacing unsafe string copy operations with length-aware alternatives that respect buffer boundaries.

The canonical fix for this vulnerability class involves one or more of the following approaches:

Approach 1 — Use strncpy or strlcpy with explicit size limits:

// BEFORE (vulnerable):
char path[256];
strcpy(path, argv[i] + 5);  // No bounds checking!

// AFTER (safer):
char path[256];
strncpy(path, argv[i] + 5, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';  // Ensure null termination

Approach 2 — Validate input length before copying:

// AFTER (with explicit validation):
char path[256];
size_t arg_len = strlen(argv[i] + 5);

if (arg_len >= sizeof(path)) {
    fprintf(stderr, "Error: argument too long (max %zu characters)\n",
            sizeof(path) - 1);
    exit(EXIT_FAILURE);
}

strcpy(path, argv[i] + 5);  // Now safe — length validated above

Approach 3 — Use dynamic allocation to handle arbitrary lengths:

// AFTER (fully flexible):
char *path = NULL;
size_t arg_len = strlen(argv[i] + 5);

path = malloc(arg_len + 1);
if (path == NULL) {
    perror("malloc");
    exit(EXIT_FAILURE);
}

strcpy(path, argv[i] + 5);  // Safe — buffer sized to fit input
// ... use path ...
free(path);

For the cmd buffer (lines 67-68), the same principles apply, with the additional consideration that strcat is equally dangerous:

// BEFORE (vulnerable):
char cmd[256];
strcpy(cmd, argv[0]);
strcat(cmd, ".exe");  // strcat is also bounds-unaware!

// AFTER (safer):
char cmd[260];  // sizeof(".exe") = 5, so 256 + 4 + null terminator
size_t argv0_len = strlen(argv[0]);

if (argv0_len >= sizeof(cmd) - 4) {  // Reserve space for ".exe\0"
    fprintf(stderr, "Error: program name too long\n");
    exit(EXIT_FAILURE);
}

strncpy(cmd, argv[0], sizeof(cmd) - 5);
cmd[sizeof(cmd) - 5] = '\0';
strncat(cmd, ".exe", 4);

How Does the Fix Solve the Problem?

The core security improvement is enforcing a contract between the input size and the destination buffer size. By ensuring the program either:
1. Rejects inputs that are too long, or
2. Allocates buffers large enough to hold any input

...we eliminate the condition that allows the overflow to occur. No overflow means no overwritten return address, no control flow hijacking, and no arbitrary code execution.


Prevention & Best Practices

Never Use These Functions on Untrusted Input

The following C standard library functions are inherently unsafe when used with untrusted input because they perform no bounds checking:

 strcpy()    use strncpy() or strlcpy() instead strcat()    use strncat() or strlcat() instead sprintf()   use snprintf() instead gets()      NEVER use this; it's been removed from C11 scanf("%s")  use scanf("%255s") with explicit width limit

Compiler Mitigations Are Not a Substitute for Fixing the Code

Modern compilers and operating systems provide several mitigations that raise the bar for exploitation — but they are not a fix:

Mitigation What It Does Bypass Possible?
Stack Canaries (-fstack-protector) Detects stack corruption before return Yes, with info leak
ASLR Randomizes memory layout Yes, with info leak or brute force
NX/DEP Marks stack non-executable Yes, via ROP chains
SafeStack (Clang) Separates safe/unsafe stacks Partial protection

These mitigations buy time; they don't eliminate the vulnerability. Fix the code.

Enable Compiler Warnings

GCC and Clang will warn about many unsafe usages if you ask them to:

# Compile with warnings that catch unsafe string operations
gcc -Wall -Wextra -Wformat-security -D_FORTIFY_SOURCE=2 -fstack-protector-strong tpl.c

_FORTIFY_SOURCE=2 in particular adds runtime checks that will abort the program if a buffer overflow is detected — turning a potential code execution into a crash (still bad, but better).

Use Static Analysis Tools

Don't rely on manual code review alone. These tools can automatically detect buffer overflow vulnerabilities:

  • Coverity — Industry-standard static analyzer, free for open source
  • CodeQL — GitHub's semantic code analysis engine
  • Flawfinder — Lightweight scanner specifically for C/C++ security issues
  • Semgrep — Fast, customizable static analysis with rules for unsafe C functions
  • AddressSanitizer (ASan) — Runtime memory error detector; invaluable during testing
# Compile with AddressSanitizer for testing
clang -fsanitize=address -g tpl.c -o tpl_asan
./tpl_asan --path=$(python3 -c "print('A'*300)")
# ASan will immediately report the overflow and print a stack trace

Consider Memory-Safe Languages

For new projects, seriously evaluate whether C is the right tool. Languages like Rust, Go, and even modern C++ with appropriate guidelines eliminate entire classes of memory safety vulnerabilities by design:

// Rust: buffer overflows are impossible — the compiler enforces bounds
fn build_path(arg: &str) -> Result<String, &'static str> {
    if arg.len() > 255 {
        return Err("Argument too long");
    }
    Ok(arg.to_string())  // String grows dynamically; no fixed buffer
}

Security Standards & References

This vulnerability maps to well-established security standards:


Conclusion

The stack buffer overflow in tpl.c is a stark reminder that some of the oldest vulnerability classes remain among the most dangerous. A single call to strcpy without bounds checking — a mistake that takes seconds to make — can hand an attacker complete control over a running process.

The key takeaways from this vulnerability:

  1. strcpy, strcat, sprintf, and gets are unsafe when used with untrusted input. Replace them with length-aware alternatives.
  2. Always validate input lengths before copying data into fixed-size buffers, or use dynamic allocation.
  3. Compiler mitigations help but don't fix the root cause — they raise the cost of exploitation, but a determined attacker can often bypass them.
  4. Static analysis and runtime sanitizers (ASan, Valgrind) should be part of every C/C++ project's CI pipeline.
  5. The fix is simple and the cost of not fixing is catastrophic — there is no excuse for shipping code with known buffer overflows.

Secure coding in C demands constant vigilance. Every function that touches user input is a potential attack surface. Treat all external input as hostile, validate lengths before every copy, and let your tools catch what your eyes miss.

💡 Pro Tip: If you're maintaining a C codebase, run grep -n "strcpy\|strcat\|sprintf\|gets" *.c right now. Every result is a potential security review candidate.


This vulnerability was identified and fixed by the OrbisAI Security automated security scanning platform. Automated scanning helps catch issues like this before they reach production — consider integrating security scanning into your CI/CD pipeline.


Further Reading:
- Smashing the Stack for Fun and Profit — Aleph One (Phrack, 1996)
- Writing Secure Code in C — Carnegie Mellon SEI
- Compiler Explorer — See how your C code compiles with different protections

Frequently Asked Questions

What is a stack buffer overflow?

A stack buffer overflow occurs when data written to a stack-allocated buffer exceeds its fixed size, overwriting adjacent memory including saved return addresses, potentially allowing an attacker to redirect program execution to arbitrary code.

How do you prevent stack buffer overflow in C?

Use bounded string functions like strncpy(), snprintf(), or strlcpy() that accept a maximum length parameter, always validate input lengths before copying, enable compiler protections like stack canaries (-fstack-protector), and use ASLR.

What CWE is stack buffer overflow?

CWE-121 (Stack-based Buffer Overflow), which is a child of CWE-787 (Out-of-bounds Write) and CWE-120 (Buffer Copy without Checking Size of Input).

Is stack canary protection enough to prevent stack buffer overflow?

No. Stack canaries (like GCC's -fstack-protector) detect some overflows at runtime but can be bypassed through information leaks or partial overwrites. The correct fix is to prevent the overflow entirely through bounds-checked copying.

Can static analysis detect stack buffer overflow?

Yes. Static analysis tools like Coverity, Semgrep, and compiler warnings (-Wall -Wformat-overflow) can detect unbounded strcpy() calls into fixed-size buffers. Many modern CI pipelines flag these patterns automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1027

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