Back to Blog
critical SEVERITY6 min read

How buffer overflow happens in C memcpy() without length validation and how to fix it

A critical buffer overflow vulnerability was discovered in `src/script_engine/core/script_engine_core.c` at line 392, where `memcpy` copied an error message into a buffer without validating the source length against any maximum. The fix introduces a length cap of 4096 bytes and ensures proper null-termination, preventing heap corruption and potential remote code execution through crafted script error messages.

O
By Orbis AppSec
Published June 10, 2026Reviewed June 10, 2026

Answer Summary

This is a CWE-120 buffer overflow vulnerability in C, specifically in the `_set_error_info()` function of `script_engine_core.c`, where `memcpy()` copies an unbounded error message string without length validation. The fix caps the message length at 4096 bytes before allocation and copies exactly `len` bytes followed by explicit null-termination, preventing heap overflow when attacker-controlled scripts trigger oversized error messages.

Vulnerability at a Glance

cweCWE-120
fixCap message length at 4096 bytes and explicitly null-terminate the copied string
riskHeap corruption leading to arbitrary code execution
languageC
root causeNo maximum length check before memcpy into dynamically allocated buffer
vulnerabilityBuffer overflow via unbounded memcpy

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:

  1. 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 standard malloc(). If eos_malloc has internal size limits or uses fixed-size pools, allocating len+1 bytes for an extremely large len could return a buffer smaller than requested—or succeed but corrupt the heap metadata.

  2. Race conditions or re-entrancy: If engine_rt.error_info is accessed concurrently, the unbounded write could corrupt memory being read by another thread.

  3. 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:

  1. 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.

  2. Copy exactly len bytes, not len+1 (memcpy(engine_rt.error_info, msg, len);): The original code relied on strlen() 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 where len+1 might exceed the allocated buffer.

  3. 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:

  1. 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.

  2. 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';

  3. Use bounded string functions where possible: Consider strncpy(), snprintf(), or platform-specific safe alternatives like strlcpy() which handle truncation and null-termination together.

  4. 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.

  5. Custom allocators need extra care: When using custom allocators like eos_malloc(), understand their failure modes. Standard malloc() 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 in script_engine_core.c had no upper bound on error message length, making it exploitable by any script that could trigger a long error message.
  • Copying len+1 bytes with memcpy is fragile—it assumes the null terminator is always within bounds. Explicit null-termination after copying exactly len bytes 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) in src/script_engine/core/script_engine_core.c:392
  • Missing control: No maximum length validation on the msg parameter 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 len bytes, 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.

References

Frequently Asked Questions

What is a buffer overflow in C?

A buffer overflow occurs when a program writes data beyond the boundaries of an allocated memory buffer, potentially corrupting adjacent memory, crashing the program, or allowing attackers to execute arbitrary code.

How do you prevent buffer overflow in C?

Always validate input lengths before copying data, use bounded copy functions like strncpy() or snprintf(), enforce maximum buffer sizes, and explicitly null-terminate strings after copying.

What CWE is buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers classic buffer overflows where data is copied into a buffer without first verifying that the source data fits within the destination buffer's bounds.

Is using malloc() with strlen() enough to prevent buffer overflow?

While dynamically allocating based on strlen() prevents overflow of a fixed-size buffer, it still requires careful handling—you must ensure the length is reasonable (to prevent excessive allocation) and that the copy operation matches the allocated size exactly with proper null-termination.

Can static analysis detect buffer overflow?

Yes, static analysis tools can detect many buffer overflow patterns including unbounded memcpy() calls, missing length checks, and mismatches between allocation sizes and copy lengths. Tools like Coverity, CodeQL, and custom AI-based scanners can flag these issues.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.