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 missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.