Skip to content

JS_ReadObject: reject empty bytecode and validate ROM atom index - #561

Open
iliasabk wants to merge 1 commit into
bellard:masterfrom
iliasabk:fix/readobj-bytecode-validation
Open

iliasabk wants to merge 1 commit into
bellard:masterfrom
iliasabk:fix/readobj-bytecode-validation

Conversation

@iliasabk

Copy link
Copy Markdown

Issues

Fixes #548 and fixes #550 — two out-of-bounds reads in the JS_ReadObject bytecode-deserialization path, both reported by sigdevel with deterministic ASan-verified PoCs at commit 04be246 (VERSION 2026-06-04).

Root causes

#548 — unvalidated runtime atom index in ROM_DATA mode (quickjs.c:38652)

In JS_ReadFunctionBytecode, the atom-fixup loop reads the 4-byte opcode operand with get_u32(bc_buf + pos + 1). With JS_READ_OBJ_ROM_DATA, that operand is used directly as a runtime atom index: JS_DupAtom(ctx, (JSAtom)idx). A crafted operand (e.g. 0x400) is not a constant atom, so JS_DupAtom indexes rt->atom_array[idx] — in the reporter's ASan log, 2504 bytes past the 5696-byte atom table (heap-buffer-overflow READ of size 8). An index that lands on a free-list slot is worse: js_rc(p)->ref_count++ increments through a tagged pseudo-pointer, a wild write.

#550 — zero-length bytecode accepted (quickjs.c:3879217878)

JS_ReadFunctionTag accepts a function whose declared byte_code_len is 0: the header fields parse, no bytecode bytes are copied, and byte_code_buf points at the end of the 96-byte allocation. JS_EvalFunction then instantiates and executes the value; JS_CallInternal starts the interpreter with pc = b->byte_code_buf and the first opcode fetch reads *pc — one byte past the end of the heap buffer (ASan heap-buffer-overflow READ, reporter-verified 5/5 on Linux).

A negative byte_code_len is additionally dangerous: function_size += bc.byte_code_len shrinks the allocation below the header size and bc_get_buf then copies with a negative length.

Fix

  • Reject byte_code_len <= 0 in JS_ReadFunctionTag with a SyntaxError. A real function always serializes to at least one opcode (return), so empty bytecode can only be malformed input; rejecting also closes the negative-length allocation shrink.
  • In the ROM_DATA atom-fixup branch, validate idx before JS_DupAtom: reject operands that are neither constant atoms (__JS_AtomIsConst) nor live entries of rt->atom_array (bounds check plus free-list tag-bit check), throwing SyntaxError and truncating b->byte_code_len at pos so free_bytecode_atoms only unwinds the atoms already duplicated — the same cleanup contract the bc_idx_to_atom failure path uses.

Verification

Built the reporter's poc_driver against this branch with ASan and ran both base64 reproducers:

A serialized function with byte_code_len == 0 was accepted by
JS_ReadFunctionTag: byte_code_buf pointed past the end of the
allocation and JS_CallInternal read the first opcode out of
bounds. Reject zero/negative bytecode lengths at deserialization.

In ROM_DATA mode the atom-fixup loop passed the raw 4-byte opcode
operand to JS_DupAtom without validation, so a crafted index read
out of bounds of the runtime atom table (or hit a free-list slot).
Reject operands that are neither constant atoms nor live entries
of rt->atom_array.

Fixes bellard#548, fixes bellard#550.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant