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=undefineddetects integer overflow at runtime during testing-Wconversionwarns 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_sizeinjs_realloc_array()was computed inintarithmetic, notsize_t, meaning it could overflow at values far belowSIZE_MAXon 64-bit platforms.- The fix requires only one guard line —
if (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_sizeto large values insidejs_realloc_array() - Sink:
js_realloc2(ctx, *parray, new_size * elem_size, &slack)atquickjs/quickjs.c:1914— the allocation call that receives the overflowed size - Missing control: No bounds check on the product of
new_sizeandelem_sizebefore the allocation call; the multiplication was performed in signedintarithmetic 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 tosize_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
- CWE-190: Integer Overflow or Wraparound
- CWE-122: Heap-based Buffer Overflow
- CERT C INT30-C: Ensure that unsigned integer operations do not wrap
- CERT C MEM35-C: Allocate sufficient memory for an object
- OWASP Memory Management Cheat Sheet
- Linux
reallocarray(3)man page - Semgrep rules for integer overflow
- fix: add integer overflow check in quickjs.c