Skip to content

Heap buffer overflow WRITE of 16 bytes in JS_CallInternal (quickjs.c:17523) via crafted bytecode - potential RCE #499

@Sebasteuo

Description

@Sebasteuo

Description

QuickJS (version 2025-09-13, commit d7ae12a) has a heap-buffer-overflow WRITE of 16 bytes in JS_CallInternal() at quickjs.c:17523. The VM stack frame is allocated based on the stack_size field read directly from untrusted bytecode, but push instructions (OP_push_*) write past the allocated buffer without bounds checking. This enables heap corruption with potential for arbitrary code execution.

Root Cause

  1. JS_ReadFunctionTag() (quickjs.c:38204) reads stack_size from bytecode via bc_get_leb128_u16() without validating it against actual bytecode stack depth.

  2. The VM stack is allocated in async_func_init() (quickjs.c:20334) with size based on this untrusted value:

s = js_malloc(ctx, sizeof(*s) + sizeof(JSValue) * (arg_buf_len + b->var_count + b->stack_size) + ...);
  1. During execution in JS_CallInternal(), push instructions execute *sp++ = JS_NewInt32(...) (quickjs.c:17523), writing 16 bytes (sizeof(JSValue)) per push with no bounds check against the allocated stack.

  2. If stack_size is smaller than the actual number of push operations in the bytecode, the VM writes arbitrarily past the heap buffer.

ASAN output

ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50e000000540 WRITE of size 16 at 0x50e000000540 thread T0 #0 in JS_CallInternal quickjs.c:17523 #1 in async_func_resume quickjs.c:20391 #2 in js_async_function_resume quickjs.c:20670 #3 in js_async_function_call quickjs.c:20764 #4 in JS_CallInternal quickjs.c:17445
0x50e000000540 is located 0 bytes after 160-byte region [0x50e0000004a0,0x50e000000540) allocated by thread T0 here: #0 in malloc #1 in js_def_malloc quickjs.c:1746 #4 in async_func_init quickjs.c:20334
SUMMARY: AddressSanitizer: heap-buffer-overflow quickjs.c:17523 in JS_CallInternal

Reproduction

Build with ASAN:

# Patch Makefile: CFLAGS_OPT=-g -O0 -fsanitize=address,undefined ... and comment out -flto
make CC=gcc LDFLAGS="-fsanitize=address,undefined"

Compile harness:

#include <stdio.h>
#include <stdlib.h>
#include "quickjs.h"

int main(int argc, char **argv) {
    FILE *f = fopen(argv[1], "rb");
    fseek(f, 0, SEEK_END);
    size_t fsize = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *buf = malloc(fsize);
    fread(buf, 1, fsize, f);
    fclose(f);
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    JSValue obj = JS_ReadObject(ctx, buf, fsize, JS_READ_OBJ_BYTECODE);
    if (!JS_IsException(obj)) {
        JSValue ret = JS_EvalFunction(ctx, obj);
        JS_FreeValue(ctx, ret);
    } else { JS_FreeValue(ctx, obj); }
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
    free(buf);
    return 0;
}
gcc -g -O0 -fsanitize=address,undefined -I. -o fuzz_bc fuzz_bc.c libquickjs.a -lm -lpthread -ldl
./fuzz_bc poc_heap_write.bin

PoC binary (84 bytes) attached.

Additional findings

Bytecode mutation fuzzing revealed 500 crashes in 3000 iterations (16.7% crash rate) including:

  • 172 heap OOB reads
  • 25 heap OOB WRITES (including this one)
  • 73 stack buffer overflows
  • 114 SEGV signals
  • 88 UBSan violations

Multiple distinct crash locations were identified in JS_CallInternal (6 unique lines), free_function_bytecode, lre_exec_backtrack (regex engine), js_closure2, and __JS_FreeAtom.

Impact

  • Heap corruption / potential RCE: A 16-byte controlled WRITE past a heap allocation boundary can overwrite adjacent heap metadata or objects, enabling arbitrary code execution. This is the same class of vulnerability as CVE-2023-48184 (CVSS 9.8).
  • Scope: QuickJS is embedded in serverless runtimes, IoT firmware, game engines, and developer tools. While the documentation notes bytecode should not be loaded from untrusted sources, many downstream projects do process semi-trusted bytecode, and the lack of any validation makes exploitation trivial.

Suggested Fix

Add a bytecode validation pass after JS_ReadFunctionTag() that verifies:

  1. Declared stack_size is consistent with the maximum stack depth of the bytecode
  2. All constant pool indices satisfy idx < cpool_count
  3. All closure variable indices satisfy idx < closure_var_count

Severity

This is a heap-buffer-overflow WRITE (CWE-787: Out-of-bounds Write) that can lead to arbitrary code execution. I believe this warrants a CVE assignment.

Credit

Discovered by Sebastián Alba Vives (GitHub: @Sebasteuo)

I would appreciate being credited in any CVE or advisory issued for this vulnerability. I am available to assist with further analysis or patch review.

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