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
-
JS_ReadFunctionTag() (quickjs.c:38204) reads stack_size from bytecode via bc_get_leb128_u16() without validating it against actual bytecode stack depth.
-
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) + ...);
-
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.
-
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:
- Declared
stack_size is consistent with the maximum stack depth of the bytecode
- All constant pool indices satisfy
idx < cpool_count
- 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.
Description
QuickJS (version 2025-09-13, commit d7ae12a) has a heap-buffer-overflow WRITE of 16 bytes in
JS_CallInternal()atquickjs.c:17523. The VM stack frame is allocated based on thestack_sizefield 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
JS_ReadFunctionTag()(quickjs.c:38204) readsstack_sizefrom bytecode viabc_get_leb128_u16()without validating it against actual bytecode stack depth.The VM stack is allocated in
async_func_init()(quickjs.c:20334) with size based on this untrusted value: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.If
stack_sizeis 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:
Compile harness:
PoC binary (84 bytes) attached.
Additional findings
Bytecode mutation fuzzing revealed 500 crashes in 3000 iterations (16.7% crash rate) including:
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
Suggested Fix
Add a bytecode validation pass after
JS_ReadFunctionTag()that verifies:stack_sizeis consistent with the maximum stack depth of the bytecodeidx < cpool_countidx < closure_var_countSeverity
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.