Back to Blog
medium SEVERITY8 min read

Buffer Overflow in Freestanding Runtime: How Unsafe strcpy() Puts Bare-Metal Systems at Risk

A critical buffer overflow vulnerability was discovered in the freestanding runtime's custom string library, where `strcpy()` and `memcpy()` implementations lacked any bounds checking whatsoever. In a bare-metal or kernel-like environment with no OS-level memory protection, this flaw could allow an attacker to overwrite adjacent memory regions — including function pointers and security-critical state — with arbitrary data. The fix introduces a safe `strlcpy()` implementation that enforces destin

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

Answer Summary

This is a CWE-120 (Buffer Copy Without Checking Size of Input) buffer overflow vulnerability in a freestanding C runtime's custom string library. The `strcpy()` implementation wrote data into destination buffers without any bounds checking, enabling out-of-bounds memory writes in environments with no OS-level protection. The fix introduces a `strlcpy()` implementation that accepts an explicit `size` parameter and truncates copies to `size - 1` bytes, always null-terminating the result. This prevents adjacent memory — including function pointers and security state — from being overwritten by oversized input.

Vulnerability at a Glance

cweCWE-120
fixReplaced strcpy() with a strlcpy() implementation that takes an explicit destination size and truncates input to prevent out-of-bounds writes
riskAttacker-controlled data overwrites adjacent memory, including function pointers and security-critical state, with no OS protection to stop it
languageC (freestanding / bare-metal)
root causeCustom strcpy() and memcpy() implementations wrote to destination buffers without checking or enforcing any size limit
vulnerabilityBuffer Overflow (unbounded strcpy in freestanding runtime)

Buffer Overflow in Freestanding Runtime: How Unsafe strcpy() Puts Bare-Metal Systems at Risk

Introduction

When most developers think about buffer overflows, they picture web servers or desktop applications crashing under a fuzzer's relentless probing. But some of the most dangerous buffer overflows live in a quieter, more treacherous place: freestanding (bare-metal) runtimes — the low-level C environments that power embedded systems, hypervisors, and custom kernels.

In these environments, there's no operating system to catch a rogue memory write. There's no ASLR to make exploitation unpredictable. There's no stack canary inserted by a distro's hardened toolchain. When a buffer overflows here, it overwrites whatever is next in memory — and that might be a function pointer, a security policy flag, or a cryptographic key.

This post breaks down a critical (CWE-120) buffer overflow vulnerability found in a freestanding runtime's custom string library, explains how it could be exploited, and walks through the strlcpy()-based fix that closes the door on unbounded string copies.


The Vulnerability Explained

What Is a Freestanding Runtime?

A "freestanding" C environment is one that doesn't rely on a hosted standard library (like glibc or MSVC's CRT). Instead, the project provides its own implementations of fundamental functions like memcpy, strlen, and strcpy. This is common in:

  • Embedded firmware (microcontrollers, IoT devices)
  • Bootloaders and UEFI modules
  • Custom kernels and hypervisors
  • WebAssembly runtimes compiled to bare metal

Because these environments have no OS underneath them, memory protection is entirely the programmer's responsibility.

The Vulnerable Code

The freestanding runtime in question implemented strcpy() like this:

// VULNERABLE - avs/runtime/freestanding/src/string/str.c
char *strcpy(char *dest, const char *src) {
    char *d = dest;
    while ((*d++ = *src++) != '\0');
    return dest;
}

And memcpy() similarly lacked any length validation against the destination buffer's actual capacity.

The problem is fundamental, not incidental. The strcpy() signature is:

char *strcpy(char *dest, const char *src);

Notice what's missing? There's no size parameter. The function has absolutely no way to know how large dest is. It will copy bytes from src until it hits a null terminator — no matter how many bytes that takes, and no matter how small dest is.

CWE-120: Buffer Copy Without Checking Size of Input

This is a textbook instance of CWE-120: "Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')". The CVSS score for this finding was rated Critical, and rightfully so.

How Could It Be Exploited?

Consider this scenario in a freestanding kernel context:

// Somewhere in the kernel's command processing path
char kernel_cmd_buffer[64];         // Fixed-size stack buffer
security_policy_t *active_policy;   // Lives right after it in memory

// Attacker controls 'user_input' — e.g., from a serial console or network packet
strcpy(kernel_cmd_buffer, user_input);  // 💥 No bounds check

If user_input is longer than 63 bytes (leaving room for the null terminator), strcpy() happily keeps writing past the end of kernel_cmd_buffer. In a freestanding environment with no stack guard pages, it overwrites active_policy — or a return address, or a function pointer table.

Concrete attack scenarios include:

  1. Control-flow hijacking: Overwriting a function pointer stored adjacent to the destination buffer, redirecting execution to attacker-controlled shellcode.
  2. Security policy bypass: Overwriting a flag or pointer that controls access checks, privilege levels, or cryptographic key selection.
  3. Persistent corruption: In a kernel context, corrupting heap metadata or page table entries, leading to privilege escalation.
  4. Denial of service: Even without a clean exploit, corrupting memory causes unpredictable crashes — particularly dangerous in safety-critical embedded systems.

The absence of OS-level protections (ASLR, NX, stack canaries inserted by the OS loader) makes exploitation significantly more reliable than in a typical hosted environment.


The Fix

What Changed

The fix introduces strlcpy() — a safer alternative to strcpy() that was originally developed by OpenBSD — and adds it to both the header and implementation files.

Header change (avs/runtime/freestanding/include/string.h):

// BEFORE
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);

// AFTER
char *strcpy(char *dest, const char *src);
size_t strlcpy(char *dest, const char *src, size_t size);  // ← NEW
char *strncpy(char *dest, const char *src, size_t n);

Implementation (avs/runtime/freestanding/src/string/str.c):

// SAFE REPLACEMENT
size_t strlcpy(char *dest, const char *src, size_t size) {
    size_t src_len = strlen(src);
    if (size > 0) {
        size_t copy_len = (src_len >= size) ? size - 1 : src_len;
        char *d = dest;
        const char *s = src;
        size_t i = 0;
        while (i < copy_len) {
            d[i] = s[i];
            i++;
        }
        d[copy_len] = '\0';  // Always null-terminate
    }
    return src_len;  // Returns full source length for truncation detection
}

Why strlcpy() Is the Right Tool

strlcpy() addresses strcpy()'s fundamental design flaw by requiring the caller to pass the destination buffer size:

// Old, dangerous way
strcpy(kernel_cmd_buffer, user_input);

// New, safe way
strlcpy(kernel_cmd_buffer, user_input, sizeof(kernel_cmd_buffer));

Let's unpack the three security properties this implementation provides:

1. Hard Truncation at size - 1 Bytes

size_t copy_len = (src_len >= size) ? size - 1 : src_len;

If src is longer than the destination buffer, the copy is truncated to size - 1 bytes. The buffer cannot be overflowed, regardless of input length.

2. Guaranteed Null-Termination

d[copy_len] = '\0';

Unlike strncpy() (which famously does not null-terminate when truncation occurs), strlcpy() always writes a null terminator as long as size > 0. This prevents a whole class of read-overrun bugs that follow unterminated string operations.

3. Truncation Detection via Return Value

return src_len;  // Full length of src, NOT the number of bytes copied

The function returns the total length of the source string, not the number of bytes written. This allows callers to detect truncation:

size_t written = strlcpy(dest, src, sizeof(dest));
if (written >= sizeof(dest)) {
    // Truncation occurred — handle the error!
    log_error("Input truncated: source was %zu bytes, buffer is %zu", 
              written, sizeof(dest));
    return ERR_INPUT_TOO_LONG;
}

This is a significant improvement over strncpy(), which gives callers no indication that truncation occurred.

Comparison: strcpy vs strncpy vs strlcpy

Property strcpy strncpy strlcpy
Bounds checking ❌ None ✅ Truncates ✅ Truncates
Always null-terminates ✅ (if no overflow) ❌ Not on truncation ✅ Always
Detects truncation ✅ Via return value
Safe for untrusted input ⚠️ Partial ✅ Yes

Prevention & Best Practices

1. Ban strcpy() in Security-Sensitive Code

In any codebase that handles untrusted input — especially freestanding runtimes, parsers, and network stacks — strcpy() should be treated as a forbidden function. Add a compiler warning or linter rule:

# GCC/Clang: warn on dangerous string functions
CFLAGS += -Wdeprecated-declarations

Or use a static analysis tool (see below) to flag its use automatically.

2. Prefer strlcpy() Over strncpy()

Many developers reach for strncpy() as the "safe" alternative to strcpy(). It's not. strncpy() does not null-terminate the destination when truncation occurs, which leads to a different class of bugs. Use strlcpy() instead, and always check the return value for truncation.

3. Use Static Analysis Tools

Several tools can catch unbounded string operations automatically:

  • Clang Static Analyzer — Detects buffer overflows and dangerous function calls
  • Coverity — Enterprise-grade, excellent at CWE-120 detection
  • Flawfinder — Lightweight, specifically targets dangerous C/C++ patterns
  • CodeQL — GitHub-native, query-based analysis with buffer overflow rules
  • AddressSanitizer (ASan) — Runtime detection; invaluable during testing

4. Adopt a Secure String Library

For freestanding environments, consider adopting or auditing a well-reviewed secure string library:

  • safeclib — Implements the C11 Annex K "bounds-checking interfaces" (strcpy_s, memcpy_s, etc.)
  • OpenBSD's strlcpy/strlcat — Battle-tested, widely ported
  • C11 Annex K (strcpy_s) — If your toolchain supports it

5. Fuzz Your String-Handling Code

Buffer overflows in string functions are exactly what fuzzers are designed to find. Tools like AFL++ and libFuzzer can be pointed at any function that accepts string input and will quickly surface overflows that manual review misses.

6. Security Standards & References


Conclusion

Buffer overflows in custom string libraries are a reminder that security doesn't come for free when you roll your own primitives. In hosted environments, the OS and standard library provide a safety net of hardened implementations, ASLR, and stack canaries. In freestanding runtimes, that net doesn't exist — every unsafe function call is a direct line to memory corruption.

The key takeaways from this vulnerability and its fix:

  • strcpy() is inherently unsafe for untrusted input because it has no mechanism to know the destination buffer's size. Treat it as a forbidden function.
  • strncpy() is not a safe replacement — it doesn't null-terminate on truncation, which trades one bug class for another.
  • strlcpy() is the right tool: it truncates safely, always null-terminates, and returns the source length so callers can detect and handle truncation.
  • Freestanding environments demand extra diligence — the absence of OS-level memory protections makes buffer overflows far easier to exploit reliably.
  • Automated scanning works — this vulnerability was caught by an automated multi-agent AI scanner before it reached production. Invest in static analysis as part of your CI/CD pipeline.

Secure coding in C is hard, but it's not mysterious. The rules are well-understood, the tools to enforce them exist, and the fixes — like the strlcpy() implementation shown here — are straightforward. The only question is whether you apply them before or after an attacker does.


This vulnerability was automatically detected and fixed by OrbisAI Security. Automated security scanning can catch issues like this before they reach production.

Frequently Asked Questions

What is a buffer overflow in a freestanding C runtime?

It occurs when a string or memory copy function writes more bytes than the destination buffer can hold. In a freestanding (bare-metal) runtime there is no OS memory protection, so the overflow silently corrupts whatever data sits in adjacent memory — potentially function pointers, return addresses, or security flags.

How do you prevent buffer overflow in C bare-metal string libraries?

Replace unbounded functions like strcpy() with size-limited alternatives such as strlcpy() or strncpy(). Always pass the exact byte capacity of the destination buffer, and validate that the source length does not exceed it before copying.

What CWE is buffer overflow from strcpy()?

CWE-120 — "Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')". Related identifiers include CWE-122 (Heap-Based Buffer Overflow) and CWE-121 (Stack-Based Buffer Overflow) depending on where the destination buffer is allocated.

Is using strncpy() enough to prevent buffer overflow in C?

Not always. strncpy() does not guarantee null-termination when the source is longer than the limit, which can cause subsequent string operations to read past the buffer. strlcpy() is safer because it always null-terminates and returns the full source length so callers can detect truncation.

Can static analysis detect unbounded strcpy() usage in C?

Yes. Tools like Semgrep, Coverity, CodeQL, and clang-tidy all have rules that flag calls to strcpy(), gets(), and similar unbounded functions. Enabling compiler warnings (-Wdeprecated-declarations, -D_FORTIFY_SOURCE=2) also catches many of these patterns at build time.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #106

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.