Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issues
Fixes #548 and fixes #550 — two out-of-bounds reads in the
JS_ReadObjectbytecode-deserialization path, both reported by sigdevel with deterministic ASan-verified PoCs at commit04be246(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 withget_u32(bc_buf + pos + 1). WithJS_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, soJS_DupAtomindexesrt->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:38792→17878)JS_ReadFunctionTagaccepts a function whose declaredbyte_code_lenis 0: the header fields parse, no bytecode bytes are copied, andbyte_code_bufpoints at the end of the 96-byte allocation.JS_EvalFunctionthen instantiates and executes the value;JS_CallInternalstarts the interpreter withpc = b->byte_code_bufand 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_lenis additionally dangerous:function_size += bc.byte_code_lenshrinks the allocation below the header size andbc_get_bufthen copies with a negative length.Fix
byte_code_len <= 0inJS_ReadFunctionTagwith aSyntaxError. 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.idxbeforeJS_DupAtom: reject operands that are neither constant atoms (__JS_AtomIsConst) nor live entries ofrt->atom_array(bounds check plus free-list tag-bit check), throwingSyntaxErrorand truncatingb->byte_code_lenatpossofree_bytecode_atomsonly unwinds the atoms already duplicated — the same cleanup contract thebc_idx_to_atomfailure path uses.Verification
Built the reporter's
poc_driveragainst this branch with ASan and ran both base64 reproducers:AgUADAAAAAAAAAABAAAABQAEAAQAAA==): unpatched crashes withheap-buffer-overflow READ of size 8inJS_ReadFunctionBytecodeon the atom table; patched rejects withSyntaxError: invalid atom index (pos=21).AQUADAAAAAAAAAABAAAAAAA=): unpatched runs the interpreter on a zero-length buffer and reachesinvalid opcodeafter reading past the allocation (the reporter's Linux ASan log documents the OOB read atJS_CallInternal); patched rejects at deserialization withSyntaxError: invalid bytecode length.make test: all tests pass (closure, language, builtin, loop, bigint, cyclic_import, worker, std, rw_handler).JS_Eval(..., JS_EVAL_FLAG_COMPILE_ONLY)→JS_WriteObject→JS_ReadObject→JS_EvalFunctionexecutes correctly — legitimate bytecode is unaffected.