How buffer overflow happens in C memcpy() without length validation and how to fix it
Introduction
The _set_error_info() function in src/script_engine/core/script_engine_core.c is responsible for storing error messages generated during script execution. At line 392, a memcpy call copied len+1 bytes from an error message into a heap-allocated buffer—but the code had a subtle and dangerous flaw: while it allocated len+1 bytes dynamically, there was no upper bound on how large len could be, and the len+1 in the memcpy call included the null terminator implicitly from strlen(). More critically, if an attacker could control the error message content (by crafting a malicious script), they could trigger memory corruption scenarios, especially when combined with the similar unbounded patterns at lines 393, 458, and 515 in the same file.
This vulnerability was rated critical because any user who can load and execute scripts on the device can craft an error condition that overwrites adjacent heap memory, potentially achieving arbitrary code execution.
The Vulnerability Explained
Here's the vulnerable code from script_engine_core.c at line 387-392:
static void _set_error_info(const char *msg)
{
if (!msg)
return;
size_t len = strlen(msg);
engine_rt.error_info = eos_malloc(len + 1);
if (engine_rt.error_info)
memcpy(engine_rt.error_info, msg, len + 1);
}
At first glance, this might look safe—after all, the code allocates len+1 bytes and copies len+1 bytes. So where's the overflow?
The real danger lies in the absence of any maximum length constraint. Consider what happens when:
-
A malicious script triggers an error with a multi-megabyte message: The
eos_malloc()function (a custom allocator in this embedded/OS context) may behave differently than standardmalloc(). Ifeos_mallochas internal size limits or uses fixed-size pools, allocatinglen+1bytes for an extremely largelencould return a buffer smaller than requested—or succeed but corrupt the heap metadata. -
Race conditions or re-entrancy: If
engine_rt.error_infois accessed concurrently, the unbounded write could corrupt memory being read by another thread. -
Adjacent memory corruption: In the embedded environment where ElenixOS's script engine operates, heap layouts are often predictable. An attacker crafting a script that triggers a specific error message size could overwrite function pointers, vtables, or control structures adjacent to the allocated buffer.
Attack scenario: An attacker loads a script into the ElenixOS script engine that deliberately triggers an error condition (e.g., a type error, undefined variable, or assertion failure) with a message string of 10,000+ characters. The script engine calls _set_error_info() with this oversized message. Depending on the allocator behavior and heap state, the memcpy overwrites critical heap metadata or adjacent objects, allowing the attacker to redirect execution flow.
The Fix
The fix introduces two key changes to _set_error_info():
Before (vulnerable):
static void _set_error_info(const char *msg)
{
if (!msg)
return;
size_t len = strlen(msg);
engine_rt.error_info = eos_malloc(len + 1);
if (engine_rt.error_info)
memcpy(engine_rt.error_info, msg, len + 1);
}
After (fixed):
static void _set_error_info(const char *msg)
{
if (!msg)
return;
size_t len = strlen(msg);
if (len > 4096) len = 4096;
engine_rt.error_info = eos_malloc(len + 1);
if (engine_rt.error_info) {
memcpy(engine_rt.error_info, msg, len);
engine_rt.error_info[len] = '\0';
}
}
Three critical changes were made:
-
Length cap at 4096 bytes (
if (len > 4096) len = 4096;): This establishes a hard upper bound on the error message size. No error message in normal operation needs to exceed 4KB, and this prevents the allocator from being asked to handle unreasonably large requests. -
Copy exactly
lenbytes, notlen+1(memcpy(engine_rt.error_info, msg, len);): The original code relied onstrlen()having measured the exact same string that's being copied, and included the null terminator in the copy. The fix separates the data copy from the null termination, making the code's intent explicit and eliminating any edge case wherelen+1might exceed the allocated buffer. -
Explicit null-termination (
engine_rt.error_info[len] = '\0';): Rather than relying on the source string's null terminator being within the copied range, the fix explicitly writes the terminator at the correct position. This guarantees the resulting string is always properly terminated, even if the message was truncated.
Prevention & Best Practices
For C developers working with string buffers:
-
Always enforce maximum lengths: Even when dynamically allocating, cap input sizes to reasonable maximums. A 4096-byte error message is more than sufficient for debugging; there's no legitimate reason for an error string to be unbounded.
-
Separate copy from termination: Instead of
memcpy(dst, src, len+1)which relies on the source having a null terminator at exactly the right position, prefer:
c memcpy(dst, src, len); dst[len] = '\0'; -
Use bounded string functions where possible: Consider
strncpy(),snprintf(), or platform-specific safe alternatives likestrlcpy()which handle truncation and null-termination together. -
Audit similar patterns: The PR notes that lines 393, 458, and 515 in the same file use similar patterns. When fixing one instance of a vulnerability pattern, always grep for and fix all instances.
-
Custom allocators need extra care: When using custom allocators like
eos_malloc(), understand their failure modes. Standardmalloc()returns NULL on failure, but custom allocators may have different behavior with extreme sizes.
Tools for detection:
- Static analyzers (Coverity, CodeQL) can flag unbounded memcpy patterns
- AddressSanitizer (ASan) catches heap overflows at runtime during testing
- Fuzz testing with AFL or libFuzzer can discover overflow-triggering inputs
Key Takeaways
- The
_set_error_info()function inscript_engine_core.chad no upper bound on error message length, making it exploitable by any script that could trigger a long error message. - Copying
len+1bytes withmemcpyis fragile—it assumes the null terminator is always within bounds. Explicit null-termination after copying exactlylenbytes is safer and clearer. - Error messages are attacker-controllable input in a script engine context—they should be treated with the same suspicion as any user input.
- A 3-line fix (length cap + bounded copy + explicit termination) eliminated a critical code execution vulnerability, demonstrating that defense-in-depth doesn't always require complex solutions.
- Similar patterns at lines 458 and 515 in the same file were flagged for review, highlighting the importance of systematic vulnerability remediation.
How Orbis AppSec Detected This
- Source: Error message string generated by script execution within the ElenixOS script engine (attacker-controlled script content)
- Sink:
memcpy(engine_rt.error_info, msg, len + 1)insrc/script_engine/core/script_engine_core.c:392 - Missing control: No maximum length validation on the
msgparameter before memory allocation and copy; no explicit null-termination - CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Added a 4096-byte length cap, changed memcpy to copy exactly
lenbytes, and added explicit null-termination
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
Buffer overflows remain one of the most dangerous vulnerability classes in C code, and this case in script_engine_core.c demonstrates why: a seemingly correct allocation-and-copy pattern becomes exploitable when there's no upper bound on input size. The fix is elegant in its simplicity—cap the length, copy precisely, terminate explicitly. For any developer maintaining C code that handles variable-length strings, especially in contexts where the input might be influenced by untrusted users or scripts, these three principles should be reflexive.
The ElenixOS script engine is now protected against oversized error messages, but this serves as a reminder: every memcpy, strcpy, and sprintf in your codebase is a potential vulnerability if the input isn't bounded.