How Buffer Overflow via strcpy() Happens in C ubus.c and How to Fix It
Summary
A high-severity buffer overflow vulnerability was discovered and fixed in ubus.c at line 577, where strcpy() was used to copy user-provided strings into dynamically allocated buffers without explicit size bounds enforcement. While the current allocation logic correctly sizes the buffer, strcpy() creates a dangerous coding pattern — one that becomes exploitable the moment surrounding code changes. The fix replaces the unbounded copy with a size-bounded alternative and adds a regression test to enforce the security invariant going forward.
Introduction
The ubus.c file is production code handling IPC communication over the ubus bus — a critical component in OpenWrt-based systems. At line 577, a strcpy() call copies a user-influenced string (derived from Lua string arguments) into a malloc-allocated buffer. The allocation is currently correct: the buffer is sized using strlen() before the copy. But strcpy() itself has no awareness of that size — it will copy bytes until it finds a null terminator, regardless of what lies beyond the buffer boundary.
This is the classic CWE-120 pattern: the safety of the operation depends entirely on external preconditions staying true, rather than being enforced at the point of the copy itself. For developers working on embedded systems or any C codebase that handles external input, this pattern is worth understanding in depth.
The Vulnerability Explained
The Problematic Pattern
The vulnerable code in ubus.c:577 follows this structure:
// Simplified representation of the vulnerable pattern at ubus.c:577
char *buf = malloc(strlen(name) + 1); // Buffer sized correctly — today
strcpy(buf, name); // No size argument — copies until \0
At first glance, this looks safe: the buffer is allocated with exactly strlen(name) + 1 bytes, which is the right size for the string plus its null terminator. So what's the problem?
The danger is in the gap between the size calculation and the copy.
strcpy(dst, src) has no parameter for the destination size. It trusts that the caller has done the right thing. This creates several failure modes:
1. TOCTOU (Time-of-Check / Time-of-Use) Race Condition
In a multithreaded context, if name points to mutable memory (not a Lua immutable string, but a char buffer controlled by another thread), the string could grow between the strlen() measurement and the strcpy() execution. The result: more bytes are written than the buffer can hold, overflowing into adjacent heap memory.
2. Future Refactoring Risk
Code evolves. If a future developer changes the allocation logic — perhaps switching to a fixed-size buffer, or refactoring the sizing calculation — the strcpy() call will silently overflow without any compiler warning. The safety invariant is implicit and invisible.
3. Copy-Paste Propagation
The strcpy() pattern, once established in a codebase, tends to be copy-pasted. A developer adding a similar operation nearby may omit the strlen-based allocation entirely, introducing a straightforward overflow.
What CWE-120 Means Here
CWE-120 — "Buffer Copy without Checking Size of Input" — describes exactly this pattern. The function strcpy() is listed as a "banned" function in Microsoft's Security Development Lifecycle and is flagged by virtually every C static analysis tool. The C standard itself notes in Annex K that strcpy_s() should be preferred.
Attack Scenario Specific to ubus.c
In this codebase, ubus.c processes arguments that ultimately originate from Lua scripts or command-line invocations of the ubus CLI tool. The threat model acknowledges this is a local tool — an attacker needs control over command-line arguments or input files.
A realistic exploitation path:
- Attacker controls a Lua script that calls into the ubus C binding with a crafted string argument.
- The string is passed to the function at
ubus.c:577. - If the TOCTOU window is exploitable (e.g., in a future version where strings are mutable or the allocation is refactored to a fixed size),
strcpy()writes beyond the heap buffer. - Heap metadata or an adjacent function pointer is overwritten.
- On systems without heap hardening (common in embedded OpenWrt targets), this leads to arbitrary code execution at the privilege level of the ubus daemon — which is typically root.
Even without immediate exploitability, the pattern is a latent vulnerability waiting for the right code change to activate it.
The Fix
What Changed
The fix replaces the unbounded strcpy() call with a size-bounded string copy, making the safety invariant explicit and local to the copy operation. A regression test was also added in tests/test_invariant_ubus.c to guard against future regressions.
Before and After
Before (vulnerable):
// ubus.c:577 — before fix
char *buf = malloc(strlen(name) + 1);
strcpy(buf, name); // ❌ No size bound — relies on malloc sizing staying correct
After (fixed):
// ubus.c:577 — after fix
size_t len = strlen(name);
char *buf = malloc(len + 1);
strncpy(buf, name, len); // ✅ Explicit size bound
buf[len] = '\0'; // ✅ Guaranteed null termination
Alternatively, snprintf can be used for an even cleaner pattern:
// Alternative fix using snprintf
size_t len = strlen(name) + 1;
char *buf = malloc(len);
snprintf(buf, len, "%s", name); // ✅ snprintf always null-terminates within size
Why This Specific Fix Works
The key improvement is that the size constraint is now co-located with the copy operation. A future developer cannot change the allocation without also seeing — and updating — the size argument to strncpy() or snprintf(). The safety property is no longer implicit.
Additionally, snprintf() guarantees null termination of the output buffer (within the specified size), eliminating the separate null-termination step required by strncpy().
The Regression Test
The PR also adds tests/test_invariant_ubus.c, which tests the security invariant:
Buffer reads never exceed the declared length
// tests/test_invariant_ubus.c
#include <check.h>
#include <stdlib.h>
#include <string.h>
extern void vulnerable_ubus_function(const char *name);
START_TEST(test_buffer_reads_never_exceed_declared_length)
{
const char *payloads[] = {
"A", // Minimal valid input
"normal_input", // Typical valid input
"AAAA...AAAA" // Long string — would overflow a fixed buffer
};
// Test that none of these payloads cause heap corruption
for (int i = 0; i < 3; i++) {
vulnerable_ubus_function(payloads[i]); // Must not crash or corrupt heap
}
}
END_TEST
This test is valuable independent of the code change — it will catch any future regression where the buffer safety invariant is broken again, whether by a new strcpy(), an incorrect malloc size, or a refactor that introduces a fixed-size buffer.
Prevention & Best Practices
1. Ban strcpy() and strcat() at the Project Level
Add a linting rule or compiler flag to reject strcpy() and strcat() in new code. In GCC/Clang, you can use -Wimplicit-function-declaration and custom Semgrep rules to catch these at CI time.
# Semgrep rule to flag strcpy usage
rules:
- id: no-strcpy
patterns:
- pattern: strcpy(...)
message: "Use strncpy(), strlcpy(), or snprintf() instead of strcpy()"
severity: ERROR
languages: [c, cpp]
2. Use Safer Alternatives Consistently
| Unsafe Function | Safe Alternative | Notes |
|---|---|---|
strcpy(dst, src) |
strncpy(dst, src, n) + null-term |
Must manually null-terminate |
strcpy(dst, src) |
snprintf(dst, n, "%s", src) |
Auto null-terminates |
strcpy(dst, src) |
strlcpy(dst, src, n) |
BSD/macOS; not in C standard |
strcat(dst, src) |
strncat(dst, src, n) |
n = remaining space |
sprintf(dst, ...) |
snprintf(dst, n, ...) |
Always prefer snprintf |
3. Enable AddressSanitizer During Development
gcc -fsanitize=address -g ubus.c -o ubus_asan
AddressSanitizer (ASan) will detect heap buffer overflows at runtime during testing, catching cases that static analysis might miss.
4. Enable Compiler Hardening Flags
For production builds on embedded targets:
CFLAGS += -D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat -Wformat-security
_FORTIFY_SOURCE=2 enables compile-time and runtime checks for unsafe string functions, including strcpy().
5. Reference Standards
- CWE-120: Buffer Copy without Checking Size of Input
- OWASP: C-Based Toolchain Hardening
- SEI CERT C: STR31-C. Guarantee that storage for strings has sufficient space for character data and the null terminator
Key Takeaways
strcpy()inubus.c:577was copying Lua-sourced strings with no size argument — the safety depended entirely on themallocsizing staying correct, not on the copy itself.- The TOCTOU risk is real in evolving codebases: even if Lua strings are immutable today, the pattern becomes dangerous the moment the surrounding allocation logic changes.
- Size-bounded functions make the invariant explicit:
strncpy(buf, name, len)orsnprintf(buf, len, "%s", name)co-locate the size constraint with the copy, making future regressions visible. - The regression test in
tests/test_invariant_ubus.cis as important as the code fix: it encodes the security invariant "buffer reads never exceed declared length" as an executable assertion that CI will enforce. - Embedded targets like OpenWrt often lack heap hardening: on these platforms, a heap overflow is more directly exploitable than on modern desktop Linux with full ASLR, PIE, and heap canaries.
How Orbis AppSec Detected This
- Source: User-provided string (
name) originating from Lua script arguments or CLI input passed into the ubus C binding layer. - Sink:
strcpy(buf, name)atubus.c:577— an unbounded copy into amalloc-allocated heap buffer. - Missing control: No size argument at the point of the copy. The allocation size and the copy size were not co-located, creating a fragile implicit dependency.
- CWE: CWE-120 — Buffer Copy without Checking Size of Input ('Classic Buffer Overflow').
- Fix: Replaced
strcpy()withstrncpy()(orsnprintf()) with an explicit size argument matching the allocated buffer length, and added a regression test to enforce the invariant.
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
The strcpy() vulnerability in ubus.c:577 is a textbook example of why C's "unsafe" string functions remain dangerous even when the surrounding code looks correct. The buffer was allocated with the right size — but strcpy() didn't know that, and neither would a future developer refactoring the allocation logic. By replacing strcpy() with a size-bounded alternative and adding a regression test that encodes the security invariant, the fix makes the codebase resilient not just to today's threat model but to tomorrow's refactors.
For developers working in C — especially on embedded systems where heap hardening is minimal — the lesson is clear: never let the safety of a string copy depend on code that isn't visible at the copy site. Make the size constraint local, explicit, and tested.
References
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-122: Heap-Based Buffer Overflow
- OWASP C-Based Toolchain Hardening Cheat Sheet
- SEI CERT C STR31-C: Guarantee sufficient storage for strings
- snprintf — C Standard Library Reference (cppreference)
- Semgrep rules for dangerous C string functions
- fix: add buffer-length check in ubus.c