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

high

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

A high-severity integer overflow vulnerability was discovered in QuickJS's libregexp.c where multiplication to compute allocation size could wrap around, causing a heap overflow. The fix replaces the unsafe `malloc(sizeof(capture[0]) * lre_get_alloc_count(bc))` pattern with `calloc(lre_get_alloc_count(bc), sizeof(capture[0]))`, which safely handles the multiplication internally and prevents exploitation.

medium

How integer overflow in bounds checking happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the W_Read function of DOOM/w_file.c that allowed attackers to bypass bounds checking by crafting WAD files with malicious offset values near UINT_MAX. The fix implements a two-step validation approach that first checks if the offset exceeds the file length, then safely calculates the remaining bytes without risk of overflow.

medium

How integer overflow in tensor shape validation happens in C++ with OpenVINO and how to fix it

A medium-severity integer overflow vulnerability was discovered in the OpenVINO noise suppression plugin where model input tensor shapes were loaded without dimension validation. An attacker could supply a crafted `.xml/.bin` model file with extremely large or zero-sized dimensions, causing integer overflow during memory allocation or zero-size allocations followed by out-of-bounds writes. The fix introduces a `NS_MAX_SHAPE_DIM` constant that validates each dimension against a safe upper bound b

medium

How integer overflow in _MALLOC() happens in C emulator memory allocation and how to fix it

A critical integer overflow vulnerability was discovered in `i286c/i286c.c` at line 216, where the expression `_MALLOC(size + 16)` could wrap around to a tiny value when `size` approaches `UINT32_MAX`. This undersized allocation leads to a massive heap buffer overflow when the emulator writes the expected number of bytes. The fix adds a simple overflow guard that checks whether `size + 16` would wrap before performing the allocation.

critical

How integer overflow in regexJIT.c heap allocation happens in C and how to fix it

A critical integer overflow vulnerability in `regex_src/regexJIT.c` allowed crafted regex patterns to trigger a heap buffer overflow by causing an unchecked multiplication of `sizeof(struct stack_item) * dfa_size` to wrap around on 32-bit platforms, resulting in an undersized allocation. The fix adds a pre-allocation overflow guard that returns `REGEX_MEMORY_ERROR` before any dangerous write can occur. Left unpatched, this vulnerability could be exploited to corrupt heap memory, crash the proces

critical

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `app/src/main/cpp/samp/settings.cpp` where `sprintf()` writes to a fixed 127-byte buffer (`char buff[0x7F]`) without bounds checking. If the `g_pszStorage` global variable contains a string longer than ~107 bytes, the formatted output exceeds the buffer, enabling stack corruption. The fix replaces `sprintf()` with `snprintf()` using `sizeof(buff)` to guarantee writes never exceed the declared buffer length.