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
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed 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)

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

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #106

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.