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:
- Control-flow hijacking: Overwriting a function pointer stored adjacent to the destination buffer, redirecting execution to attacker-controlled shellcode.
- Security policy bypass: Overwriting a flag or pointer that controls access checks, privilege levels, or cryptographic key selection.
- Persistent corruption: In a kernel context, corrupting heap metadata or page table entries, leading to privilege escalation.
- 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
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-787: Out-of-bounds Write
- OWASP: Buffer Overflow
- SEI CERT C Coding Standard: STR07-C: Use the bounds-checking interfaces for string manipulation
- NIST NVD: CWE-120
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.