Back to Blog
critical SEVERITY8 min read

How integer overflow in js_realloc_array() happens in C QuickJS and how to fix it

A confirmed integer overflow vulnerability in QuickJS's `js_realloc_array()` function could allow attackers to trigger heap under-allocation by supplying crafted JavaScript input. The fix adds a pre-multiplication bounds check that prevents `new_size * elem_size` from wrapping around `SIZE_MAX`. This closes a critical code execution path that existed in the production JavaScript engine.

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

Answer Summary

This vulnerability is an integer overflow (CWE-190) in the C function `js_realloc_array()` inside `quickjs/quickjs.c`. When the product of `new_size` and `elem_size` exceeds `SIZE_MAX`, the multiplication silently wraps to a small value, causing `js_realloc2()` to allocate a far smaller buffer than expected — a classic heap under-allocation primitive. The fix adds a pre-multiplication overflow guard (`if (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size) return -1;`) that safely rejects any size combination that would overflow, eliminating the vulnerability entirely.

Vulnerability at a Glance

cweCWE-190
fixAdded pre-multiplication guard `if (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size) return -1;`
riskHeap under-allocation leading to potential buffer overwrite and memory corruption
languageC
root cause`new_size * elem_size` multiplication performed without overflow check before passing to `js_realloc2()`
vulnerabilityInteger Overflow in Size Calculation (CWE-190)

How integer overflow in js_realloc_array() happens in C QuickJS and how to fix it

Summary

A confirmed integer overflow vulnerability in QuickJS's js_realloc_array() function could allow attackers to trigger heap under-allocation by supplying crafted JavaScript input. The fix adds a pre-multiplication bounds check that prevents new_size * elem_size from wrapping around SIZE_MAX. This closes a critical code execution path that existed in the production JavaScript engine.


Introduction

The quickjs/quickjs.c file is the heart of the QuickJS JavaScript engine — a compact, embeddable JS runtime used in production applications across many platforms. Deep inside this file, the function js_realloc_array() is responsible for growing internal arrays as the JavaScript engine processes scripts. A flaw in this function's size calculation, present at line 1911, meant that a sufficiently crafted JavaScript payload could silently cause the engine to allocate a buffer far smaller than intended.

What made this particularly telling is that the original code contained a developer comment acknowledging the risk:

/* XXX: potential arithmetic overflow */
new_size = max_int(req_size, *psize * 3 / 2);
new_array = js_realloc2(ctx, *parray, new_size * elem_size, &slack);

The comment was correct. The overflow was real. And until this fix, it was unmitigated.


The Vulnerability Explained

What goes wrong in js_realloc_array()

The function signature and local variables look like this:

static no_inline int js_realloc_array(JSContext *ctx, void **parray,
                                       int elem_size, int *psize, int req_size)
{
    int new_size;
    size_t slack;
    void *new_array;
    /* XXX: potential arithmetic overflow */
    new_size = max_int(req_size, *psize * 3 / 2);
    new_array = js_realloc2(ctx, *parray, new_size * elem_size, &slack);

The critical line is:

new_array = js_realloc2(ctx, *parray, new_size * elem_size, &slack);

Here, new_size is an int and elem_size is also an int. Their product is computed as a signed integer multiplication before being implicitly converted to size_t for the allocator. If new_size is large (e.g., driven upward by req_size from attacker-controlled JavaScript) and elem_size is a multi-byte type (e.g., 8 bytes for a 64-bit pointer), the product can exceed INT_MAX or SIZE_MAX and wrap around to a tiny value.

For example:
- new_size = 536870913 (just over 2²⁹)
- elem_size = 8
- Product: 536870913 * 8 = 4294967304, which overflows a 32-bit int to 8

The allocator receives a request for 8 bytes when the code intended to allocate 4 GB. The engine then writes into that 8-byte buffer as if it were gigabytes in size — a classic heap buffer overflow.

How an attacker exploits this

An attacker who can supply JavaScript input to a QuickJS-powered application (a plugin host, a server-side JS sandbox, a desktop app with a JS scripting layer) could craft a script that forces js_realloc_array() to be called with a req_size large enough to trigger the overflow. The subsequent write into the under-allocated buffer corrupts adjacent heap memory, which is a well-established primitive for achieving arbitrary code execution in native applications.

The attack surface is any application that:
1. Embeds QuickJS as a scripting engine
2. Allows untrusted or semi-trusted JavaScript input
3. Runs the engine in a process with meaningful privileges

Real-world impact

Because QuickJS is frequently embedded in desktop applications (including Tauri-based apps), browser extensions, IoT firmware, and developer tools, this vulnerability could affect a wide range of downstream consumers. The js_realloc_array() function is called during normal array growth operations — a code path that virtually every non-trivial JavaScript program exercises.


The Fix

Before and after

The fix is surgical: three lines changed in quickjs/quickjs.c around line 1911.

Before (vulnerable):

    /* XXX: potential arithmetic overflow */
    new_size = max_int(req_size, *psize * 3 / 2);
    new_array = js_realloc2(ctx, *parray, new_size * elem_size, &slack);

After (fixed):

    new_size = max_int(req_size, *psize * 3 / 2);
    if (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size)
        return -1;
    new_array = js_realloc2(ctx, *parray, (size_t)new_size * elem_size, &slack);

Why this fix works

The guard condition uses the standard safe multiplication check for unsigned integers:

if (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size)
    return -1;

This reads as: "If the element size is non-zero, and new_size is larger than the maximum value that can be multiplied by elem_size without overflowing size_t, then abort."

This is mathematically equivalent to checking new_size * elem_size > SIZE_MAX without performing the overflow itself. It is the canonical C idiom for safe size multiplication, recommended by CERT C rule INT30-C.

The secondary change casts the multiplication operands explicitly to size_t:

new_array = js_realloc2(ctx, *parray, (size_t)new_size * elem_size, &slack);

This ensures the multiplication happens in size_t arithmetic (unsigned, pointer-width) rather than signed int arithmetic, eliminating signed integer overflow undefined behavior as well.

The /* XXX: potential arithmetic overflow */ comment is also removed — because the overflow is now impossible, the warning is no longer needed.

The regression test

A new test file test/test_invariant_quickjs.c was added to guard against future regressions. It exercises lre_realloc with boundary values including SIZE_MAX, SIZE_MAX / 2 + 1, and SIZE_MAX / 4 * 3 — exactly the values that would trigger overflow in the unpatched code. The test verifies that each call either succeeds with a valid allocation or fails gracefully, and never returns an under-allocated buffer that could be silently misused.


Prevention & Best Practices

1. Use reallocarray() where available

On Linux (glibc 2.26+) and BSD systems, reallocarray(ptr, n, size) performs the overflow check internally:

// Safe: reallocarray checks n * size overflow internally
new_array = reallocarray(*parray, new_size, elem_size);

This is the simplest way to avoid the class of bug entirely in new code.

2. Apply the pre-division check consistently

When reallocarray() is not available (e.g., for portability), always use the pre-division pattern before any n * size calculation passed to an allocator:

if (n != 0 && size > SIZE_MAX / n) {
    /* handle error */
}
result = malloc(n * size);

3. Enable compiler and sanitizer warnings

  • -fsanitize=integer (Clang) or -fsanitize=undefined detects integer overflow at runtime during testing
  • -Wconversion warns on implicit narrowing conversions that can hide overflow
  • AddressSanitizer (-fsanitize=address) catches the heap overwrite that results from under-allocation

4. Audit all malloc(a * b) patterns

Use Semgrep or CodeQL to find every instance of malloc(expr * expr) or realloc(ptr, expr * expr) in your codebase and verify each one has an overflow guard. The QuickJS codebase even self-documented the risk with a comment — static analysis would have flagged this automatically.

5. Follow CERT C secure coding standards

  • INT30-C: Ensure that unsigned integer operations do not wrap
  • MEM35-C: Allocate sufficient memory for an object
  • CWE-190: Integer Overflow or Wraparound
  • CWE-122: Heap-based Buffer Overflow (the downstream consequence)

Key Takeaways

  • The /* XXX: potential arithmetic overflow */ comment in the original QuickJS source was a known-but-unmitigated risk — a clear signal that a security review was overdue for this function.
  • new_size * elem_size in js_realloc_array() was computed in int arithmetic, not size_t, meaning it could overflow at values far below SIZE_MAX on 64-bit platforms.
  • The fix requires only one guard lineif (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size) return -1; — demonstrating that integer overflow mitigations are often low-cost to implement once identified.
  • Any application embedding QuickJS and accepting untrusted JavaScript input was potentially vulnerable to heap corruption via this code path.
  • Regression tests with boundary values (SIZE_MAX, SIZE_MAX / 2 + 1) are essential for size-calculation bugs because normal test inputs never reach the overflow boundary.

How Orbis AppSec Detected This

  • Source: Attacker-controlled JavaScript input that drives req_size to large values inside js_realloc_array()
  • Sink: js_realloc2(ctx, *parray, new_size * elem_size, &slack) at quickjs/quickjs.c:1914 — the allocation call that receives the overflowed size
  • Missing control: No bounds check on the product of new_size and elem_size before the allocation call; the multiplication was performed in signed int arithmetic with no overflow detection
  • CWE: CWE-190 — Integer Overflow or Wraparound
  • Fix: Added if (elem_size != 0 && (size_t)new_size > SIZE_MAX / elem_size) return -1; before the allocation call and cast the multiplication to size_t

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

Integer overflow in size calculations is one of the oldest and most dangerous vulnerability classes in C programming — and one of the easiest to overlook precisely because the arithmetic looks correct at first glance. The js_realloc_array() case is a textbook example: a two-variable multiplication, an implicit type conversion, and a missing pre-check. The original developer even left a comment acknowledging the risk, yet the fix went unimplemented.

The remediation is a single guard line that costs essentially nothing in performance but eliminates an entire class of heap corruption attack. For developers working on embedded engines, language runtimes, or any C code that allocates memory based on user-influenced sizes: audit your malloc(a * b) and realloc(ptr, a * b) call sites today. The pattern is mechanical to check and the consequences of missing it are severe.


References

Frequently Asked Questions

What is an integer overflow vulnerability?

An integer overflow occurs when an arithmetic operation produces a value larger than the maximum the data type can hold, causing it to silently wrap around to a small (often zero or near-zero) value. In memory allocation contexts, this means requesting far less memory than intended.

How do you prevent integer overflow in C memory allocation?

Before multiplying two size values together, check whether the product would exceed SIZE_MAX using the pattern `if (a != 0 && b > SIZE_MAX / a)`. Standard libraries like `reallocarray()` on BSD/Linux perform this check automatically.

What CWE is integer overflow?

Integer overflow in C is classified as CWE-190 (Integer Overflow or Wraparound). When it leads to under-allocation of heap memory, it frequently chains into CWE-122 (Heap-based Buffer Overflow).

Is using size_t enough to prevent integer overflow in C?

No. Using `size_t` ensures you have the widest unsigned type for the platform, but it does not prevent the multiplication itself from overflowing. You still need an explicit pre-multiplication bounds check.

Can static analysis detect integer overflow in C?

Yes. Tools like Semgrep, Coverity, CodeQL, and clang's UBSan (Undefined Behavior Sanitizer) can flag unchecked size multiplications before calls to malloc/realloc. The QuickJS codebase even had a comment `/* XXX: potential arithmetic overflow */` at the vulnerable line, confirming the risk was known but unmitigated.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #23

Related Articles

critical

How integer overflow in CsoundMYFLTArray constructor happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in `Java/cs_glue.cpp` at line 324, where the `CsoundMYFLTArray` constructor multiplied a user-controlled integer `n` by `sizeof(MYFLT)` without checking for overflow before passing the result to `malloc`. An attacker supplying a value near `INT_MAX` could trigger the overflow, causing an undersized heap allocation that subsequent writes would overflow. The fix adds an explicit `SIZE_MAX / sizeof(MYFLT)` guard and replaces `malloc` with `ca

high

How integer overflow in malloc happens in C bipartite matching and how to fix it

A high-severity integer overflow vulnerability was discovered in the bipartite matching algorithm implementation where unchecked multiplication operations for memory allocation could wrap around, causing undersized buffer allocations and subsequent heap overflow. The fix replaces vulnerable `malloc(sizeof(int) * V)` patterns with safe `calloc(V, sizeof(int))` calls and adds proper bounds validation to prevent exploitation.

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How integer overflow in buffer size calculation happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

critical

How integer overflow happens in C reliable.c and how to fix it

A critical integer overflow vulnerability was discovered in `reliable.c` at line 1299, where the `packet_buffer_size` calculation used signed `int` arithmetic that could wrap to a negative or undersized value when large `fragment_size` values were involved. By casting each operand to `size_t` before multiplication, the fix eliminates the overflow risk entirely and ensures the allocated buffer is always large enough to hold the reassembled packet data.

critical

How integer overflow in buffer size calculation happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in OpenCV's HAL filter implementation where multiplying image dimensions without overflow protection could allocate dangerously undersized buffers. An attacker supplying crafted image dimensions (e.g., 65536×65536) could trigger heap corruption through out-of-bounds writes. The fix promotes the calculation to 64-bit arithmetic with a single cast.