Skip to content

JS_ReadObject: validate bytecode stack usage against declared frame size - #562

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

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

Conversation

@iliasabk

Copy link
Copy Markdown

Issue

Fixes #551JS_ReadObject() trusted the stack_size field of deserialized
function bytecode. A malformed stream could declare fewer operand stack slots
than the bytecode actually uses; JS_CallInternal() then allocates the frame
with alloca() sized by the declared counts and reads/writes past it
(ASan dynamic-stack-buffer-overflow, CWE-125/CWE-787).

Root cause

JS_CallInternal() lays out the interpreter frame as
arg_buf | var_buf | stack_buf | var_refs, sized entirely from serialized
header fields (arg_count, var_count, stack_size, var_ref_count).
The deserializer never verified that the bytecode stays inside the declared
operand-stack region, so undersized stack_size (or plain stack underflow
in the bytecode) slid sp out of the allocation.

Fix

After JS_ReadFunctionBytecode() completes in JS_ReadFunctionTag(), the
function's bytecode is now run through the compiler's existing stack-depth
analysis (compute_stack_size), refactored to take (bc_buf, bc_len)
instead of a JSFunctionDef so the reader can share it. The check rejects
with SyntaxError when:

  • the bytecode needs more operand-stack slots than b->stack_size
    declares (the demonstrated undersized-frame case), or
  • the bytecode is malformed in ways the analysis already detects —
    invalid opcodes, out-of-bounds jump targets, inconsistent stack depth,
    stack underflow (which is what the issue's 18-byte PoC actually does:
    it pops from an empty operand stack at pc=0).

One fix point covers every path that produces executable function
bytecode: top-level functions, modules (JS_ReadModule reads its
func_obj through the same tag) and cpool-nested functions.

Test

tests/bjson.c now accepts a 5th read argument enabling
JS_READ_OBJ_BYTECODE, and test_bjson.js gains
bjson_test_undersized_stack_frame() feeding the exact 17-byte PoC stream
from the issue — asserted to be rejected with SyntaxError.

Verification

  • Unpatched (04be246): PoC driver dies with ASan
    dynamic-stack-buffer-overflow READ in JS_CallInternal
    (quickjs.c:18267), matching the issue trace.
  • Patched: JS_ReadObject throws SyntaxError, clean exit, no sanitizer
    findings.
  • Positive case: script compiled with JS_EVAL_FLAG_COMPILE_ONLY,
    serialized via JS_WriteObject(JS_WRITE_OBJ_BYTECODE), read back and
    executed via JS_EvalFunction returns the correct result — legitimate
    bytecode is not rejected.
  • make test suites pass (test_bjson incl. the new regression test,
    test_builtin --std, test_language, test_bigint,
    test_cyclic_import, test_closure, test_loop).

Scope note

This validates the operand-stack dimension of the frame. Operand indices
into var_buf/arg_buf/var_refs (e.g. get_loc beyond var_count)
are a distinct hardening surface, intentionally left out to keep the diff
reviewable.

Deserialized function bytecode was trusted to match its declared
stack_size. A malformed stream could declare fewer operand stack slots
than the bytecode uses, making JS_CallInternal() read/write past its
stack-allocated frame (dynamic-stack-buffer-overflow, issue bellard#551).

Reuse the compiler's bytecode graph exploration (compute_stack_size)
at read time: it computes the maximum operand stack depth actually
needed and additionally rejects invalid opcodes, out-of-bounds jump
targets and stack underflows. Functions whose bytecode needs more
slots than declared are refused with a SyntaxError.

compute_stack_size() now takes (bc_buf, bc_len) instead of a
JSFunctionDef so the reader can share it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]dynamic-stack-buffer-overflow read in JS_CallInternal: undersized call frame for the executed bytecode (CWE-125)

1 participant