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

medium

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

high

How Arbitrary Code Execution via Template Imports Happens in JavaScript (lodash) and How to Fix It

A high-severity arbitrary code execution vulnerability (CVE-2026-4800) was discovered in lodash's template function, specifically in how it handles the `imports` option with untrusted input. The fix upgrades lodash from version 4.17.21 to 4.18.0 in the project's `package.json` and `yarn.lock`, eliminating the attack surface where crafted template imports could execute arbitrary code on the server.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js IP address parsing and how to fix it

A critical SSRF vulnerability (CVE-2026-69192) was discovered in the ip-address npm package version 10.2.0, which could allow attackers to bypass IP address validation and access internal services. The fix upgrades the dependency to version 10.3.1, which properly handles edge cases in IP address parsing that previously allowed trust-boundary bypasses.

critical

How Sensitive Data Exposure happens in Python web applications and how to fix it

A critical sensitive data exposure vulnerability was discovered in `nodes/google_gemini.py` where the Google Gemini API key was returned in plaintext through a web endpoint. The fix masks the token in API responses, preventing credential theft from any client that queries the token endpoint. This protects downstream users of this Node.js library from unauthorized access to their Google Gemini services.

high

How Authentication Bypass happens in Next.js App Router with Turbopack and how to fix it

A critical authentication bypass vulnerability (CVE-2026-64642) was discovered in Next.js versions prior to 16.2.11, specifically affecting App Router applications using Turbopack with a single locale configuration. This vulnerability allowed attackers to bypass middleware and proxy protections, potentially gaining unauthorized access to protected routes and resources that should have been secured by authentication checks.

critical

How SQL Injection Happens in CSV-to-SQL Converters and How to Fix It

A critical SQL injection vulnerability was discovered in the `csv2sql()` function in `src/data/converter/csv.js`, where CSV data and table names were directly interpolated into SQL INSERT statements without sanitization. The fix implements input validation through identifier sanitization and proper value escaping, eliminating the attack surface while preserving legitimate functionality.