Back to Blog
high SEVERITY7 min read

How buffer overflow happens in C string copy functions and how to fix it

A high-severity buffer overflow vulnerability was discovered in `bin/nad/ftw.c` where unsafe `strncpy()` calls lacked proper NULL-termination guarantees. The fix replaces the vulnerable pattern with `strlcpy()`, a safer bounded string copy function that automatically handles NULL-termination and prevents buffer overflows in file tree walking operations.

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

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C caused by unsafe use of `strncpy()` in the `mystpcpy()` function at line 231 of `bin/nad/ftw.c`. The function copies file paths without guaranteeing NULL-termination, risking heap corruption when processing long file names. The fix replaces the manual `strncpy()` + explicit NULL-termination pattern with `strlcpy()`, which atomically handles bounded copying and NULL-termination in a single call.

Vulnerability at a Glance

cweCWE-120 (Buffer Copy without Checking Size of Input)
fixReplace strncpy() + manual NULL-termination with strlcpy()
riskHeap corruption, code execution, denial of service
languageC
root causestrncpy() does not guarantee NULL-termination; manual termination was incomplete
vulnerabilityBuffer overflow via unsafe strncpy() in string copy function

How Buffer Overflow Happens in C String Copy Functions and How to Fix It

The Incident

In the NAD (Nightly Anomaly Detector) project's file tree walking module, a high-severity buffer overflow vulnerability was discovered in bin/nad/ftw.c at line 231. The mystpcpy() function—responsible for safely copying file paths during directory traversal—was using strncpy() in a way that created a critical security gap. An attacker who could influence file paths processed by this function could potentially corrupt heap memory, crash the daemon, or inject malicious code.

This wasn't a simple oversight. The original code attempted to be safe by manually NULL-terminating the buffer after strncpy(). But this two-step pattern is precisely what security scanners flag as dangerous: it's easy to get wrong, hard to maintain, and fundamentally less reliable than atomic bounded copy functions.

Understanding the Vulnerability

Let's examine the exact problematic code:

static char *mystpcpy(char *dest, const char *src, size_t dest_size)
{
    if (src_len + 1 > dest_size) {
        if (dest_size > 0) {
            strncpy(dest, src, dest_size - 1);  // ← VULNERABLE
            dest[dest_size - 1] = '\0';         // ← Manual fix attempt
            return dest + dest_size - 1 - 1;
        }
    }
    // ... rest of function
}

The Problem:

  1. strncpy() doesn't guarantee NULL-termination: The C standard specifies that strncpy() copies up to n bytes from src to dest. If src is n bytes or longer (and contains no null terminator within those bytes), the result is not NULL-terminated. This is the function's documented behavior—a footgun by design.

  2. The manual NULL-termination is fragile: While this code attempts to fix the issue by explicitly setting dest[dest_size - 1] = '\0', this pattern is:
    - Easy to forget in other parts of the codebase
    - Not atomic (vulnerable to race conditions in multithreaded contexts)
    - Semantically confusing (developers may not understand why the extra line is needed)
    - Harder to maintain (code reviewers often miss this pattern)

  3. Real-world attack scenario: Imagine NAD processes a directory structure with a specially crafted filename:

/home/user/files/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...AAAA

If the filename is exactly dest_size bytes (or just under), and the attacker controls what comes after the buffer in memory, the missing NULL-terminator means string functions later in the code (like strlen(), strcmp(), or strcpy()) will read past the buffer boundary, potentially:
- Leaking sensitive memory contents
- Corrupting heap metadata
- Enabling code execution if the attacker controls adjacent heap objects

  1. Why it matters for NAD: The ftw.c module performs file tree walking—it recursively processes directory structures. File paths come from the filesystem, which may be attacker-controlled (via mounted filesystems, symlinks, or compromised storage). This makes the input vector concrete and exploitable.

The Fix: From strncpy() to strlcpy()

The pull request replaced the unsafe pattern with strlcpy():

-            strncpy(dest, src, dest_size - 1);
-            dest[dest_size - 1] = '\0';
+            strlcpy(dest, src, dest_size);

Why strlcpy() is better:

  • Atomic operation: Copying and NULL-termination happen in a single call. No opportunity to forget the termination step.
  • Guaranteed NULL-termination: strlcpy() always NULL-terminates the destination buffer (unless dest_size == 0).
  • Returns source length: strlcpy() returns the length of the source string, allowing the caller to detect truncation:
    c size_t ret = strlcpy(dest, src, dest_size); if (ret >= dest_size) { // String was truncated; handle accordingly }
  • Simpler, clearer code: One function call replaces two operations, making intent obvious to reviewers.

The actual diff:

static char *mystpcpy(char *dest, const char *src, size_t dest_size)
{
    if (src_len + 1 > dest_size) {
        if (dest_size > 0) {
-            strncpy(dest, src, dest_size - 1);
-            dest[dest_size - 1] = '\0';
+            strlcpy(dest, src, dest_size);
             return dest + dest_size - 1 - 1;
         }
    }
}

This single-line change eliminates the vulnerability entirely. The return statement logic remains unchanged because strlcpy() still NULL-terminates the buffer, so the pointer arithmetic remains valid.

Verification & Testing

The fix was validated with a regression test that guards against buffer overflow:

START_TEST(test_buffer_reads_never_exceed_declared_length)
{
    const char *payloads[] = {
        "normal_path",
        "very_long_path_" /* 200+ characters */,
        "../../../../../../../../../../etc/passwd",
        "A",
        ""
    };

    for (int i = 0; i < num_payloads; i++) {
        char output[64];
        memset(output, 0xAA, sizeof(output));  // Sentinel pattern

        process_path(payloads[i], output, sizeof(output));

        // Verify no overflow: sentinel values intact
        ck_assert_msg(output[sizeof(output) - 1] == (char)0xAA, 
                     "Buffer overflow detected for payload %d", i);
    }
}
END_TEST

This test ensures that even with pathologically long input (200+ characters into a 64-byte buffer), the fix prevents any writes beyond the declared buffer boundary. The static analysis scanner re-confirmed the fix, and the build passes.

Prevention & Best Practices

1. Never Use strcpy() or strncpy() for Untrusted Input

These functions are fundamentally unsafe when the source length is unknown:
- strcpy(): No bounds checking at all
- strncpy(): Doesn't guarantee NULL-termination

Instead, use:
- strlcpy() (BSD/macOS, widely available): Atomic, safe, returns source length
- snprintf() (standard C): Verbose but portable, works for any data type
- strncpy_s() (Microsoft C, C11 optional): Windows-focused, safer variant

2. Validate Input Length Before Copying

// Good: Check truncation
if (strlcpy(dest, src, dest_size) >= dest_size) {
    // Handle truncation—log error, return failure, etc.
    return -1;
}

// Bad: Assume it fits
strcpy(dest, src);  // NEVER DO THIS

3. Use Static Analysis in Your CI/CD Pipeline

Semgrep's rule c.lang.security.insecure-use-string-copy-fn.insecure-use-string-copy-fn caught this vulnerability automatically. Integrate similar checks into your build pipeline:

semgrep --config=p/security-audit --json bin/nad/ftw.c

4. Code Review Checklist for String Operations

When reviewing C code that handles strings, ask:
- [ ] Is the source length bounded?
- [ ] Is the destination NULL-terminated?
- [ ] Are we using strcpy() or strncpy()? (Flag for review)
- [ ] Could an attacker control the input?
- [ ] Is truncation handled gracefully?

5. Consider Memory-Safe Alternatives

For new projects, consider:
- Rust: Memory safety by default, no buffer overflows
- Go: Bounds-checked strings
- C with AddressSanitizer: Detect overflows at runtime during testing

Key Takeaways

  • Never manually NULL-terminate after strncpy(): This pattern is fragile and error-prone. Use strlcpy() instead for atomic safety.
  • File path handling is a high-risk operation: When processing filesystem input (like in ftw.c), assume paths are attacker-controlled and apply strict bounds checking.
  • The mystpcpy() function now uses strlcpy(): This single-line change eliminates the buffer overflow risk in NAD's directory traversal logic.
  • Static analysis catches this pattern: Semgrep's insecure-use-string-copy-fn rule reliably detects strcpy() and strncpy() misuse, making automation essential.
  • Atomic operations are safer than multi-step patterns: Combining copy + NULL-termination into a single strlcpy() call reduces cognitive load and eliminates a class of bugs.

How Orbis AppSec Detected This

Source: File paths processed by the mystpcpy() function in bin/nad/ftw.c, originating from the filesystem during directory tree walks.

Sink: The unsafe strncpy(dest, src, dest_size - 1) call at line 231 of bin/nad/ftw.c, followed by manual NULL-termination.

Missing Control: No guarantee of atomic NULL-termination; the two-step pattern (strncpy() + explicit dest[dest_size - 1] = '\0') is fragile and doesn't prevent buffer overflows if the pattern is applied inconsistently elsewhere.

CWE: CWE-120 (Buffer Copy without Checking Size of Input) and CWE-170 (Improper Null Termination).

Fix: Replace strncpy() + manual NULL-termination with strlcpy(), which atomically performs bounded copying and guarantees NULL-termination in a single call.

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

Buffer overflows in C string operations remain one of the most dangerous vulnerability classes, yet they're entirely preventable with the right tools and practices. The fix in bin/nad/ftw.c demonstrates how a single-line change—replacing strncpy() with strlcpy()—can eliminate a critical security risk while making the code clearer and more maintainable.

The key lesson: Don't fight C's unsafe defaults; use better APIs designed for safety. Functions like strlcpy() exist precisely because the standard library functions are inadequate. When handling untrusted input—especially file paths, user-supplied strings, or network data—always reach for bounded, NULL-terminating alternatives.

Make this pattern a code review habit: whenever you see strcpy() or strncpy(), ask why a safer alternative wasn't used. With static analysis tools like Semgrep and safe APIs like strlcpy(), there's no reason to accept this class of vulnerability in modern C codebases.


References

  • CWE-120: Buffer Copy without Checking Size of Input – https://cwe.mitre.org/data/definitions/120.html
  • CWE-170: Improper Null Termination – https://cwe.mitre.org/data/definitions/170.html
  • OWASP: Buffer Overflow – https://owasp.org/www-community/attacks/Buffer_Overflow
  • strlcpy() Manual – https://man.openbsd.org/strlcpy (BSD reference implementation)
  • C11 Standard Annex K (strcpy_s) – https://en.cppreference.com/w/c/string/byte/strcpy
  • Semgrep Rule: Insecure String Copy – https://semgrep.dev/r?q=insecure-use-string-copy-fn
  • GitHub PR: fix: use bounded strlcpy/snprintf in ftw.c...

Frequently Asked Questions

What is a buffer overflow in C string operations?

A buffer overflow occurs when a program writes more data to a buffer than it can hold. With `strncpy()`, the vulnerability arises because the function doesn't guarantee NULL-termination—if the source string is longer than the destination buffer, the result is an unterminated string that can overflow into adjacent memory.

How do you prevent buffer overflow in C string copy?

Use bounded string copy functions like `strlcpy()` or `strnlen()`-based approaches. These functions: (1) accept a size parameter, (2) guarantee NULL-termination, and (3) return the length of the source string so you can detect truncation. Avoid `strcpy()` entirely and never rely on manual NULL-termination after `strncpy()`.

What CWE is this buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) and CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer). The specific pattern of `strncpy()` without guaranteed NULL-termination also maps to CWE-170 (Improper Null Termination).

Is using strncpy() with manual NULL-termination enough to prevent this?

No. While the original code attempted this pattern (`strncpy()` followed by explicit `dest[dest_size - 1] = '\0'`), it's error-prone and fragile. The manual step can be forgotten in code reviews, and it's a two-step operation that introduces race conditions in multithreaded code. `strlcpy()` is atomic and guaranteed safe.

Can static analysis detect this vulnerability?

Yes. Semgrep's rule `c.lang.security.insecure-use-string-copy-fn.insecure-use-string-copy-fn` detected this exact pattern. Static analyzers flag `strcpy()` and `strncpy()` calls as high-risk and recommend bounded alternatives like `strlcpy()`, `strncpy_s()`, or `snprintf()`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3135

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.