Heap-buffer-overflow in add_fast_array_element() via String.prototype.split("")

Open Beginner friendly
#601 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
66/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c
Domain
security

Research direction

Start in quickjs.c at expand_fast_array() and add_fast_array_element(), then reproduce the supplied String.prototype.split("") case with an AddressSanitizer build. Check the capacity boundary against the write at quickjs.c:9766 and verify the fix removes the reported out-of-bounds write; the issue names no regression test.

Written by the indexing model from the issue text.

Description

Heap-buffer-overflow in add_fast_array_element() via String.prototype.split("")

Affected commit: 712e8c0ca26949477c38f91dbaf955cc48e2f261 (2026-11-02)
Severity: High — 8-byte out-of-bounds write past heap allocation
Reproducibility: 100%
Found by: coverage-guided fuzzing with AddressSanitizer


ASan Stack Trace
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x616000001188
WRITE of size 8 at 0x616000001188 thread T0

    #0  add_fast_array_element      quickjs.c:9766
    #1  JS_CreateProperty           quickjs.c:10361
    #2  JS_DefineProperty           quickjs.c:10870
    #3  JS_DefinePropertyValue      quickjs.c:10913
    #4  JS_DefinePropertyValueValue quickjs.c:10930
    #5  js_string_split             quickjs.c:48797
    #6  js_call_c_function          quickjs.c:18073
    #7  JS_CallInternal             quickjs.c:18280
    #8  JS_CallInternal             quickjs.c:18684
    #9  JS_CallFree                 quickjs.c:21306
    #10 JS_EvalFunctionInternal     quickjs.c:38319
    #11 __JS_EvalInternal           quickjs.c:38452
    #12 JS_Eval                     quickjs.c:38519

0x616000001188 is 0 bytes past the end of a 520-byte heap region allocated by js_realloc2 (from expand_fast_array).

Root Cause

expand_fast_array() (quickjs.c:9723) — computes new capacity:

static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len)
{
    uint32_t new_size;
    size_t slack;
    JSValue *new_array_prop;
    /* XXX: potential arithmetic overflow */
    new_size = max_int(new_len, p->u.array.u1.size * 3 / 2);
    new_array_prop = js_realloc2(ctx, p->u.array.u.values,
                                 sizeof(JSValue) * new_size, &slack);
    ...
    p->u.array.u1.size = new_size;
}

add_fast_array_element() (quickjs.c:9749) — capacity check and write:

static inline int add_fast_array_element(JSContext *ctx, JSObject *p, ...)
{
    uint32_t new_len = p->u.array.count + 1;
    ...
    if (unlikely(new_len > p->u.array.u1.size)) {     // ← boundary: `>` should be `>=`
        expand_fast_array(ctx, p, new_len);
    }
    p->u.array.u.values[new_len - 1] = val;           // ← OOB write at line 9766
}

The off-by-one: when p->u.array.u1.size already equals new_len (i.e., the buffer was allocated for exactly new_len elements in a previous expand_fast_array call with no slack returned by js_realloc2), the > check passes without expanding. The subsequent write to values[new_len - 1] uses new_len = count + 1, which is size + 1 — writing one JSValue (8 bytes) past the end of the buffer.

The trigger path via js_string_split repeatedly calls JS_CreateDataPropertyUint32 for each split element, hitting this boundary condition after the array reaches a specific allocation size.

PoC (81 bytes, minimized)
"auuuuuuuuuPpr( a= !d=uitr a= !d=uitrnŰ{ buŰ, buint8/////tbc".split("").r===

To reproduce:

# Build with ASan
clang -fsanitize=address -g -o qjs_asan quickjs.c libregexp.c libunicode.c cutils.c quickjs-libc.c

# Run PoC
./qjs_asan -i poc.js
Suggested Fix

One-line change — use >= instead of > so the buffer is expanded when new_len equals the current capacity:

--- a/quickjs.c
+++ b/quickjs.c
@@ -9763,7 +9763,7 @@ static inline int add_fast_array_element(JSContext *ctx, JSObject *p,
         }
     }
-    if (unlikely(new_len > p->u.array.u1.size)) {
+    if (unlikely(new_len >= p->u.array.u1.size)) {
         if (expand_fast_array(ctx, p, new_len)) {
             JS_FreeValue(ctx, val);
             return -1;
Dominant language
C
Stars
11.1k
Forks
1.2k
Avg merge
11h 20m
Merged PRs (30d)
1

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from bellard/quickjs

All issues in bellard/quickjs

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.