Back to Blog
critical SEVERITY8 min read

Buffer Overflow in C: How Unsafe strcpy Puts Your App at Risk

A critical buffer overflow vulnerability was discovered in `sisyphus/board.c` where unsafe C string functions were used without bounds checking, opening the door to memory corruption, crashes, and potential code execution. The fix replaces unbounded functions like `strcpy` with size-bounded alternatives such as `strlcpy` and `snprintf`, enforcing a hard limit on how much data can be written into any buffer. Understanding this class of vulnerability is essential for any developer working with C o

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

Answer Summary

A buffer overflow in C's sisyphus/board.c used unsafe strcpy() without bounds checking (CWE-120), risking memory corruption and code execution. The fix replaces strcpy() with strlcpy() and sprintf() with snprintf(), adding explicit size limits to prevent writing beyond allocated buffer boundaries. This is a critical vulnerability in low-level C code where memory safety is not automatically enforced.

Vulnerability at a Glance

cweCWE-120 (Buffer Copy without Checking Size of Input)
fixReplace with bounded alternatives (strlcpy, snprintf) that enforce maximum buffer sizes
riskMemory corruption, denial of service, arbitrary code execution
languageC
root causeUse of unbounded string functions (strcpy, sprintf) without size validation
vulnerabilityBuffer Overflow via Unsafe String Functions

Buffer Overflow in C: How Unsafe strcpy Puts Your App at Risk

Introduction

Few vulnerability classes have haunted software engineering as persistently as the buffer overflow. First documented in the 1970s and famously weaponized in the 1988 Morris Worm, buffer overflows remain a top cause of exploitable security bugs in C and C++ software today. Despite decades of awareness, they keep appearing in production codebases — including, as we'll explore here, a recent critical finding in sisyphus/board.c.

If you write C, maintain a legacy C codebase, or work on any system that links against native code, this post is for you. We'll walk through exactly what went wrong, how an attacker could exploit it, what the fix looks like, and how to prevent this class of bug from sneaking into your code in the first place.


The Vulnerability Explained

What Is a Buffer Overflow?

A buffer overflow occurs when a program writes more data into a fixed-size block of memory (a "buffer") than that buffer was allocated to hold. The excess data spills over into adjacent memory, corrupting whatever happens to live there — other variables, control structures, or even return addresses on the call stack.

In C, this happens most easily through string-handling functions that don't know — or don't check — the size of their destination buffer.

The Vulnerable Code

The vulnerability was found at sisyphus/board.c:389, flagged by the Semgrep rule utils.custom.buffer-overflow-strcpy. The root cause: an unsafe C buffer function used without size checking.

The classic offenders look like this:

// ❌ UNSAFE — strcpy does not check destination buffer size
char dest[256];
strcpy(dest, user_supplied_input);

// ❌ UNSAFE — strcat does not check remaining space
char buf[256] = "Hello, ";
strcat(buf, username);

// ❌ UNSAFE — sprintf does not limit output length
char message[256];
sprintf(message, "Welcome, %s!", username);

In each of these cases, if user_supplied_input, username, or any other source string is longer than the destination buffer allows, the write operation happily marches past the end of the buffer, overwriting adjacent memory.

How Could It Be Exploited?

Buffer overflows are not just crash bugs — in the right conditions, they are code execution vulnerabilities. Here's a simplified attack scenario:

Scenario: Stack-Based Buffer Overflow

Imagine board.c processes a board name submitted by a user over a network connection:

void process_board_name(const char *input) {
    char name_buf[256];
    strcpy(name_buf, input);  // Vulnerable line
    // ... further processing
}

The stack frame for process_board_name looks roughly like this in memory:

[ name_buf (256 bytes) ][ saved registers ][ return address ]

If an attacker sends a carefully crafted string of 300+ bytes, the overflow can reach and overwrite the return address. When process_board_name returns, instead of jumping back to the legitimate caller, the CPU jumps to an address the attacker controls — potentially shellcode embedded in the payload itself, or a gadget in an existing library (a technique called Return-Oriented Programming, or ROP).

What an attacker can achieve:
- Crash the application (Denial of Service)
- Corrupt program state (logic bypass, privilege escalation)
- Execute arbitrary code (full system compromise)
- Leak sensitive memory contents (information disclosure)

Real-World Impact

This vulnerability is classified as HIGH severity and maps to CWE-120: Buffer Copy without Checking Size of Input. In a production application, exploitation could mean:

  • Remote code execution if the affected function processes network input
  • Privilege escalation if the process runs with elevated permissions
  • Data corruption leading to silent integrity failures
  • Application crashes causing service outages

The Fix

What Changed

The fix replaces unbounded string functions with size-bounded alternatives that accept an explicit maximum length parameter, ensuring writes can never exceed the declared buffer size.

Before (vulnerable):

// ❌ No size limit — will overflow if input > 255 chars
char dest[256];
strcpy(dest, source);

// ❌ No format output limit
char msg[256];
sprintf(msg, "Board: %s", board_name);

After (safe):

// ✅ strlcpy: copies at most sizeof(dest)-1 bytes, always null-terminates
char dest[256];
strlcpy(dest, source, sizeof(dest));

// ✅ snprintf: limits output to sizeof(msg)-1 chars, always null-terminates
char msg[256];
snprintf(msg, sizeof(msg), "Board: %s", board_name);

Why This Works

The key insight is explicit size propagation. By passing sizeof(dest) directly to the copy function:

  1. The compiler knows the buffer size at the call site
  2. The function enforces the limit — it will truncate rather than overflow
  3. Null termination is guaranteedstrlcpy and snprintf always null-terminate within bounds
  4. The intent is documented — future readers immediately see the buffer is size-bounded

A Note on strncpy vs strlcpy

You might wonder: why not just use strncpy? This is a common point of confusion.

// ⚠️ strncpy — prevents overflow, but does NOT guarantee null termination
char dest[256];
strncpy(dest, source, sizeof(dest));
// If source >= 256 chars, dest[255] is NOT set to '\0' — still dangerous!

strncpy was designed for fixed-width record fields, not general string handling. If the source is exactly as long as (or longer than) the buffer, it leaves the destination without a null terminator, which causes the next string operation to run off the end of the buffer anyway.

strlcpy (available on BSD, macOS, and via libbsd on Linux) always null-terminates and returns the length of the source string, making it easy to detect truncation:

size_t result = strlcpy(dest, source, sizeof(dest));
if (result >= sizeof(dest)) {
    // Truncation occurred — handle it explicitly
    fprintf(stderr, "Warning: input truncated from %zu to %zu bytes\n",
            result, sizeof(dest) - 1);
}

The Security Invariant

The PR establishes a clear security invariant that should be enforced going forward:

Buffer reads never exceed the declared length.

This invariant is verifiable, testable, and unambiguous — exactly what a good security property looks like.


Prevention & Best Practices

1. Ban Unsafe Functions at the Compiler Level

Many teams use compiler flags or static analysis to forbid the most dangerous functions outright:

# GCC/Clang: treat use of dangerous functions as errors
CFLAGS += -Werror=implicit-function-declaration

# Or use _FORTIFY_SOURCE for runtime checks
CFLAGS += -D_FORTIFY_SOURCE=2 -O2

You can also use #pragma or a custom header to poison unsafe symbols:

// security_guards.h
#pragma GCC poison strcpy strcat sprintf gets

Any use of these functions will now fail at compile time.

2. Use Static Analysis in CI/CD

The vulnerability in this case was caught by Semgrep, an excellent open-source static analysis tool. Add it to your pipeline:

# .github/workflows/security.yml
- name: Run Semgrep
  uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/c.lang.security.insecure-use-string-copy-without-size
      p/owasp-top-ten

Other tools worth integrating:
- CodeQL — deep semantic analysis, free for open source
- Coverity — industry-standard for C/C++
- AddressSanitizer (ASan) — runtime detection of memory errors
- Valgrind — memory error detection and profiling

3. Write Fuzz Tests

Fuzzing is one of the most effective techniques for finding buffer overflows:

// fuzz_board.c — AFL/libFuzzer harness
#include <stdint.h>
#include <stddef.h>

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Feed arbitrary input to the vulnerable function
    char safe_input[size + 1];
    memcpy(safe_input, data, size);
    safe_input[size] = '\0';

    process_board_name(safe_input);
    return 0;
}

Compile with:

clang -fsanitize=fuzzer,address -o fuzz_board fuzz_board.c board.c
./fuzz_board -max_len=65536

4. Enable Memory Safety Mitigations

Modern platforms offer several layers of defense-in-depth:

Mitigation What It Does How to Enable
Stack Canaries Detects stack smashing at runtime -fstack-protector-strong
ASLR Randomizes memory layout OS-level, enabled by default on modern Linux
NX/DEP Marks stack non-executable -z noexecstack
RELRO Hardens GOT/PLT sections -Wl,-z,relro,-z,now
CFI Validates indirect calls -fsanitize=cfi (Clang)
# Recommended hardening flags for C projects
CFLAGS  += -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2
LDFLAGS += -Wl,-z,relro,-z,now -Wl,-z,noexecstack

5. Consider Memory-Safe Languages for New Code

Where feasible, new systems code written in Rust eliminates this entire class of vulnerability by design. Rust's ownership model and bounds-checked slices make buffer overflows a compile-time error, not a runtime exploit. (Notably, this project already has Rust dependencies — expanding their use could prevent future vulnerabilities of this type.)

6. Relevant Standards and References


Conclusion

Buffer overflows are old, well-understood, and entirely preventable — yet they continue to appear in real codebases every day. The fix here is elegant in its simplicity: swap strcpy for strlcpy, swap sprintf for snprintf, and pass the buffer size explicitly. Three characters and a size argument stand between your users and potential code execution.

The key takeaways from this vulnerability and its fix:

  • Never use strcpy, strcat, sprintf, or gets in new C code. They are functionally deprecated for security-sensitive contexts.
  • Always use size-bounded alternatives (strlcpy, strncat, snprintf) and pass sizeof(buffer) explicitly.
  • Check for truncation — when input is silently truncated, your program's behavior may change in ways that introduce other bugs.
  • Layer your defenses — static analysis, fuzzing, compiler hardening flags, and runtime sanitizers work together to catch what code review misses.
  • Establish and test security invariants — the invariant "buffer reads never exceed declared length" is simple, testable, and should be encoded in your CI pipeline.

Security is not a single fix — it's a culture of careful habits, automated tooling, and continuous vigilance. Every unsafe function replaced is one fewer foothold for an attacker.


This vulnerability was identified and fixed as part of an automated security scanning pipeline. Automated tools are a force multiplier for security — but they work best when developers understand the underlying principles behind the findings.

Frequently Asked Questions

What is a buffer overflow in C?

A buffer overflow occurs when data written to a buffer exceeds its allocated size, overwriting adjacent memory. In C, functions like strcpy() don't check if the source data fits in the destination buffer, making them inherently unsafe.

How do you prevent buffer overflow in C?

Always use size-bounded string functions like strlcpy(), strncat(), and snprintf() instead of strcpy(), strcat(), and sprintf(). Validate input length before copying and always know the exact size of your buffers.

What CWE is buffer overflow?

Buffer overflow vulnerabilities are primarily categorized as CWE-120 (Buffer Copy without Checking Size of Input) and CWE-121 (Stack-based Buffer Overflow). Both describe the core issue of writing beyond buffer boundaries.

Is input validation enough to prevent buffer overflow?

Input validation alone is not sufficient. You must combine validation with size-bounded functions. A developer might validate that input "looks reasonable" but still pass it to strcpy() without checking actual buffer capacity, leading to overflow.

Can static analysis detect buffer overflow?

Yes. Modern static analysis tools like Clang Static Analyzer, Valgrind, and specialized security scanners can detect strcpy() usage and flag it as high-risk. However, automated fixes that replace unsafe functions with bounded alternatives are more reliable than warnings alone.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3

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.