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

high

How DoS via sparse array deserialization happens in Svelte devalue and how to fix it

A high-severity vulnerability (CVE-2026-42570) was discovered in the devalue library version 5.7.1, used by the Astro-powered website. This vulnerability allowed attackers to trigger denial-of-service conditions through maliciously crafted sparse arrays during deserialization. The fix involved upgrading devalue from 5.7.1 to 5.8.1, which implements proper safeguards against sparse array exploitation.

critical

How heap buffer overflow via strcpy() happens in C frei0r plugins and how to fix it

A critical heap buffer overflow vulnerability was discovered in the frei0r video plugin `cairoaffineblend.c`, where `strcpy()` was used to copy user-controlled blend mode strings without any bounds checking. An attacker controlling plugin parameters could overflow the heap buffer, corrupt adjacent memory, and potentially achieve arbitrary code execution. The fix replaces `strcpy()` with bounded `memcpy()` operations and adds proper `realloc()` error handling.

critical

How command injection happens in Python os.system() and how to fix it

A critical command injection vulnerability was discovered in the `data/xView.yaml` dataset download script, where `os.system(f'rm -rf {labels}')` constructed a shell command using an f-string with a path derived from user-controlled YAML configuration. An attacker supplying a crafted dataset YAML file could embed shell metacharacters in the path to execute arbitrary commands. The fix replaces the shell invocation entirely with Python's `shutil.rmtree()`, eliminating the attack surface by never i

critical

How NULL pointer dereference happens in C gotcha_malloc() and how to fix it

A critical NULL pointer dereference vulnerability was discovered in `src/gotcha_utils.c` at line 84, where the `add_library()` function called `gotcha_malloc()` without checking whether the allocation succeeded before dereferencing the returned pointer. Because `gotcha_malloc` uses `mmap` internally, it can return `NULL` or `MAP_FAILED` under memory pressure, causing a segmentation fault that crashes the host application. The fix adds a single, targeted null check that returns early if allocatio

critical

How command injection happens in Python subprocess and how to fix it

A critical shell injection vulnerability was discovered in `utils/downloads.py` where `subprocess.check_output` was called with `shell=True` while passing a user-controlled URL parameter. This allowed attackers to inject arbitrary shell commands by embedding metacharacters like `;`, `&&`, or `$(...)` into a URL string. The fix removes `shell=True`, ensuring the URL is passed as a literal argument in a list rather than being interpreted by the shell.

critical

How buffer overflow happens in C LZSS decompression and how to fix it

A high-severity buffer overflow vulnerability was discovered in `user/libprtos/common/lzss.c`, where the LZSS decompression routine failed to validate offset and length values decoded from compressed input before using them as indices into the `text_buf` ring buffer. An attacker supplying crafted compressed data could trigger out-of-bounds reads or writes, potentially leading to memory corruption, information disclosure, or arbitrary code execution. The fix introduces strict bounds validation on