Back to Blog
critical SEVERITY8 min read

Unbounded strcpy() in FreezeProject/fs.c: How Four Lines Fixed a Critical Buffer Overflow

A critical buffer overflow vulnerability was discovered in `FreezeProject/src/fs.c`, where a custom `strcpy()` implementation was used at four separate call sites to copy user-controlled filenames into fixed-size buffers without any length checking. An attacker could supply a filename longer than the destination buffer to corrupt adjacent memory, potentially hijacking control flow or crashing the filesystem. The fix introduces a bounded `safe_strncpy()` helper that enforces the `MAX_FILENAME` li

O
By Orbis AppSec
Published June 1, 2026Reviewed June 3, 2026

Answer Summary

This is a CWE-120 buffer overflow vulnerability in C code within FreezeProject/src/fs.c. The custom strcpy() implementation copied user-controlled filenames into fixed-size buffers at four call sites without length validation, allowing attackers to overflow the buffer and corrupt adjacent memory. The fix replaces all unsafe strcpy() calls with a new safe_strncpy() helper that enforces the MAX_FILENAME boundary and ensures null-termination, preventing memory corruption and potential code execution.

Vulnerability at a Glance

cweCWE-120 (Buffer Copy without Checking Size of Input)
fixReplaced strcpy() with safe_strncpy() enforcing MAX_FILENAME limit at all four call sites
riskMemory corruption, control flow hijacking, denial of service
languageC
root causeCustom strcpy() copied user filenames to fixed buffers without length checks
vulnerabilityUnbounded buffer copy in custom strcpy() implementation

Unbounded strcpy() in FreezeProject/fs.c: How Four Lines Fixed a Critical Buffer Overflow

The Incident

The FreezeProject/src/fs.c file implements a custom in-memory and on-disk filesystem — the kind of low-level code that manages file metadata, allocates slots, mounts stored state from disk, and serializes everything back. It's exactly the sort of code that needs to handle adversarial input gracefully, because filenames come directly from user input at the shell level.

What we found instead was a custom strcpy() implementation being called at four separate locations to copy filenames into fixed-size files[i].name buffers — with zero bounds checking. This is a textbook, critical-severity stack/heap buffer overflow waiting to be triggered.


The Vulnerability Explained

The Custom strcpy() — Already a Red Flag

The file defines its own strcpy():

char* strcpy(char* dest, const char* src) {
    // ... unbounded byte-by-byte copy
    return dest;
}

Writing your own strcpy() in a security-sensitive context is almost always the wrong move. This implementation copies bytes from src to dest until it hits a null terminator — with no awareness of how large dest actually is. The moment src is longer than dest, you have undefined behavior: adjacent memory gets overwritten.

Four Vulnerable Call Sites

The dangerous function was called in four places, each copying a filename into a fixed-size buffer:

1. fs_alloc_slot() — line 57 (original)

static int fs_alloc_slot(const char* name) {
    for (int i = 0; i < MAX_FILES; i++) {
        if (!files[i].used) {
            strcpy(files[i].name, name);  // ← no length limit

2. fs_mount() — line 141 (original)

files[i].used = 1;
strcpy(files[i].name, meta->name);  // ← reading from disk, still dangerous

3. fs_create() — line 186 (original)

if (!files[i].used) {
    strcpy(files[i].name, name);  // ← user input flows here

4. fs_save() — line 327 (original)

struct fs_metadata* meta = (struct fs_metadata*)metadata_buf;
strcpy(meta->name, files[fd].name);  // ← serializing back to disk

The Attack Path

The PR description is explicit: user input from the shell flows directly into file creation functions that call strcpy. Here's what a concrete attack looks like:

  1. An attacker interacts with the FreezeProject shell interface and issues a file creation command with a filename of, say, 512 characters: AAAAAAAAAA... (×512).
  2. The shell passes this string to fs_create().
  3. fs_create() calls strcpy(files[i].name, name), where files[i].name is a fixed-size buffer (bounded by MAX_FILENAME).
  4. strcpy() happily copies all 512 bytes, blowing past the end of files[i].name and overwriting whatever sits adjacent in memory — potentially other file metadata entries, control data, or return addresses depending on the memory layout.
  5. The result ranges from silent data corruption (other files' metadata is trashed) to a full crash or, in the worst case, exploitable control-flow hijacking.

The fs_mount() call site is particularly sneaky: even data being read back from disk gets passed through the unbounded copy. If an attacker can write a crafted disk image (or if a previous overflow corrupted the on-disk metadata), the overflow fires again at mount time.


The Fix

A Minimal, Purpose-Built Safe Copy Helper

The fix adds a single static helper function, safe_strncpy(), immediately after the existing strcpy() definition:

static void safe_strncpy(char* dest, const char* src, size_t n) {
    size_t i;
    for (i = 0; i + 1 < n && src[i]; i++) {
        dest[i] = src[i];
    }
    dest[i] = 0;
}

This is a clean, dependency-free bounded copy that:
- Copies at most n - 1 bytes from src to dest
- Always null-terminates dest (the dest[i] = 0 after the loop)
- Stops early if src is shorter than n (no over-read)

The i + 1 < n condition in the loop guard is the critical detail — it ensures there is always at least one byte of space remaining for the null terminator before the loop writes the next character.

Before and After at Each Call Site

fs_alloc_slot()

// Before
strcpy(files[i].name, name);

// After
safe_strncpy(files[i].name, name, MAX_FILENAME);

fs_mount()

// Before
strcpy(files[i].name, meta->name);

// After
safe_strncpy(files[i].name, meta->name, MAX_FILENAME);

fs_create()

// Before
strcpy(files[i].name, name);

// After
safe_strncpy(files[i].name, name, MAX_FILENAME);

fs_save()

// Before
strcpy(meta->name, files[fd].name);

// After
safe_strncpy(meta->name, files[fd].name, MAX_FILENAME);

Every call site now passes MAX_FILENAME as the size bound — the same constant that defines the buffer size. This creates a consistent, enforceable contract: no filename stored anywhere in the filesystem layer can ever exceed MAX_FILENAME - 1 bytes (plus null terminator).

Why All Four Sites Matter

It might be tempting to fix only the "obvious" user-input paths (fs_alloc_slot and fs_create) and leave the others. That would be a mistake:

  • fs_mount() reads metadata from disk. If the disk image was created by a vulnerable version of this code (or crafted by an attacker), the meta->name field could contain an oversized string. Fixing the mount path closes this re-entry vector.
  • fs_save() serializes files[fd].name back to disk. If a truncated-but-valid name is in memory, this ensures the on-disk representation is also safely bounded, maintaining consistency between the in-memory and on-disk representations.

Prevention & Best Practices

1. Never Use Unbounded String Copy Functions with User Input

strcpy(), strcat(), and gets() have no place in code that processes external input. In C, always reach for bounded alternatives:

Unsafe Safer Alternative
strcpy(dst, src) strlcpy(dst, src, sizeof(dst)) or safe_strncpy()
strcat(dst, src) strlcat(dst, src, sizeof(dst))
sprintf(buf, fmt, ...) snprintf(buf, sizeof(buf), fmt, ...)
gets(buf) fgets(buf, sizeof(buf), stdin)

Note that strncpy() from the standard library is also problematic — it does not guarantee null termination when the source is longer than n. The safe_strncpy() introduced here explicitly handles this.

2. Define and Enforce Buffer Size Constants Consistently

The fix works because MAX_FILENAME is used both to declare the buffer size and as the bound in safe_strncpy(). This consistency is essential. If the buffer size and the copy limit diverge, you get off-by-one errors or re-opened vulnerabilities. Consider:

#define MAX_FILENAME 256

typedef struct {
    char name[MAX_FILENAME];  // buffer defined with constant
    // ...
} file_entry_t;

// Copy also uses the same constant
safe_strncpy(entry.name, user_input, MAX_FILENAME);

3. Treat All External Data as Adversarial — Including Disk Data

The fs_mount() call site illustrates an important principle: data read from disk is external data. It may have been written by a different version of your code, a different machine, or a malicious actor. Never assume on-disk data is safe to copy without bounds checking.

4. Use Static Analysis and Compiler Warnings

Tools that can catch this class of issue early:
- -Wall -Wformat-overflow (GCC/Clang) — catches some format string and overflow issues
- AddressSanitizer (-fsanitize=address) — runtime detection of buffer overflows during testing
- cppcheck — static analysis that flags unsafe string function usage
- clang-tidy with bugprone-not-null-terminated-result — catches misuse of string functions

5. Relevant Standards

  • CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • CWE-676: Use of Potentially Dangerous Function (specifically flags strcpy)
  • OWASP: Buffer Overflow
  • SEI CERT C: STR31-C — Guarantee that storage for strings has sufficient space for character data and the null terminator

Key Takeaways

  • Custom strcpy() implementations carry the same risks as the standard one — if you're writing your own byte-copy loop without a length parameter, you've just reinvented an unsafe function. The FreezeProject/src/fs.c custom strcpy() had no n parameter and no bounds awareness.

  • All four call sites needed fixing, not just the most obvious one — the fs_mount() and fs_save() paths are easy to overlook, but both handle data that can be attacker-influenced, making them equally dangerous.

  • safe_strncpy() always null-terminates — unlike strncpy() from the C standard library, the new helper guarantees a null terminator even when truncation occurs. This prevents a separate class of read-overrun bugs downstream.

  • Buffer size constants must be shared between declaration and copy — using MAX_FILENAME in both the struct definition and the safe_strncpy() call ensures the two stay in sync when the constant is updated.

  • Filesystem code is a high-value target — code that handles filenames, paths, and metadata is frequently exposed to user-controlled input. It deserves the same scrutiny as network-facing code.


Conclusion

This vulnerability in FreezeProject/src/fs.c is a clear example of how a seemingly simple custom helper function — an unbounded strcpy() — can create a critical security exposure when it's used at multiple points in security-sensitive code. The fix is elegant in its simplicity: an eight-line safe_strncpy() helper that enforces MAX_FILENAME at every copy site, ensuring that no amount of adversarial input can push data past the end of a filename buffer.

The broader lesson is one of discipline: in C, every string copy touching external input must be bounded, and the bound must match the actual destination buffer size. When you find yourself writing a custom string copy function, ask whether it has a length parameter — and if it doesn't, that's your first warning sign.

Security in low-level filesystem code isn't optional. The filenames that users provide are, by definition, attacker-controlled. Treat them accordingly.


This fix was identified and applied automatically by OrbisAI Security. Automated security scanning caught all four vulnerable call sites simultaneously, ensuring no partial fix left residual risk.

Frequently Asked Questions

What is a buffer overflow in strcpy()?

A buffer overflow in strcpy() occurs when the function copies a source string longer than the destination buffer can hold, writing beyond the buffer's boundaries and corrupting adjacent memory. This is especially dangerous with custom strcpy() implementations that don't include any bounds checking.

How do you prevent buffer overflow in C string operations?

Use bounded string functions like strncpy(), strlcpy(), or snprintf() that accept a maximum length parameter. Always validate input length before copying, ensure null-termination, and consider using safer alternatives like strlcpy() that guarantee null-termination or creating wrapper functions with built-in safety checks.

What CWE is buffer overflow without bounds checking?

CWE-120 (Buffer Copy without Checking Size of Input), also related to CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) and CWE-787 (Out-of-bounds Write). These are classic memory safety vulnerabilities in C/C++ code.

Is using strncpy() enough to prevent buffer overflow?

Not always. While strncpy() limits the number of bytes copied, it doesn't guarantee null-termination if the source is longer than the destination, which can cause subsequent string operations to read beyond buffer boundaries. You must manually null-terminate or use safer alternatives like strlcpy() or custom wrappers that enforce both length limits and null-termination.

Can static analysis detect unbounded strcpy() calls?

Yes, modern static analysis tools and linters can detect unsafe string operations like unbounded strcpy() calls, especially when the destination is a fixed-size buffer. Tools like Semgrep, Coverity, and compiler warnings (-Wstringop-overflow in GCC) can identify these patterns, and automated security tools like Orbis AppSec can both detect and fix them.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #22

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.