Repository navigation
Port bellard/quickjs@fcbf5ea to fix BJSON array serialization - #1748
Open
gengjiawen wants to merge 1 commit into
Open
gengjiawen wants to merge 1 commit into
gengjiawen wants to merge 1 commit into
Conversation
JS_WriteArray() read the elements with JS_GetPropertyUint32(), which runs
arbitrary JS when an element is an accessor. The array is borrowed from the
enclosing object, so a getter that drops the last reference to it frees the
array in the middle of its own serialization; the loop then keeps reading the
freed JSObject, and JS_WriteObjectRec() clears ->tmp_mark on it afterwards:
import * as bjson from "qjs:bjson";
const holder = { a: [1, 2, 3] };
Object.defineProperty(holder.a, 0, {
get() { delete holder.a; return 5; },
enumerable: true, configurable: true
});
bjson.write(holder);
ERROR: AddressSanitizer: heap-use-after-free READ of size 2
#0 js_get_fast_array_element
#1 JS_GetPropertyUint32
quickjs-ng#2 JS_WriteArray
quickjs-ng#3 JS_WriteObjectRec
quickjs-ng#4 JS_WriteObjectTag
quickjs-ng#5 js_bjson_write
freed by: free_object <- free_zero_refcount
Write the own enumerable value properties directly instead, the way
JS_WriteObjectTag() already does for plain objects, and throw a TypeError for
accessors. No JS runs during the serialization, so nothing can free the object.
Elements that are missing or non-enumerable are now written as undefined, which
means holes no longer resolve through the prototype chain.
os.Worker's postMessage() uses the same serializer and was affected as well.
Co-authored-by: Fabrice Bellard <fabrice@bellard.org>
bnoordhuis
reviewed
Sep 24, 2026
bnoordhuis
left a comment
Contributor
There was a problem hiding this comment.
Isn't an easier fix doing js_dup(obj) before the JS_GetPropertyUint32 calls, then JS_FreeValue(ctx, obj) afterwards?
Overriding array elements with getters isn't common but given the choice between properties getting lost in transit or not, I prefer 'not'. I suppose the counterargument is that getters turn into regular properties during round-tripping now, but that still seems better.
It's nice this PR writes out fast arrays more efficiently but that can be a separate PR (and it's already plenty fast, it's not a bottleneck.)
This branch has not been deployed
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.
JS_WriteArray()reads the elements withJS_GetPropertyUint32(), which runsarbitrary JS when an element is an accessor. The array is borrowed from the
enclosing object --
JS_WriteObjectTag()passesp->prop[i].u.valuewithouttaking a reference -- so a getter that drops the last reference to the array
frees it in the middle of its own serialization:
The loop keeps reading the freed
JSObject, andJS_WriteObjectRec()clears->tmp_markon it afterwards.Upstream fixed this in bellard/quickjs@fcbf5ea ("fixed BJSON array
serialization (#457)"): write the own enumerable value properties directly, the
way
JS_WriteObjectTag()already does for plain objects, and throw a TypeErrorfor accessors. No JS runs during the serialization, so nothing can free the
object under the writer.
os.Worker'spostMessage()uses the same serializer (JS_WriteObject2()withJS_WRITE_OBJ_REFERENCE), so it was affected as well; there the freed pointeralso ends up in the object reference list.
Behaviour changes
Array.prototype[1] = "x""x"undefinedTypeErrorundefinedThe last two are what already happens for the properties of a plain object.
Difference from upstream
The fast-array loop stops at
min_uint32(len, p->u.array.count), so the numberof elements written can never exceed the length written to the stream. I could
not construct a case where
count > len; it is cheap insurance for the reader.Tests
TypeErrorwith this change);WRITE_OBJ_BYTECODE-- theis_templatebranch and its non-enumerablerawproperty had no coverage,and this change rewrites both.
tests.confis 116/116,api-testpasses, andtests/test_bjson.jsplustests/test_worker.jsare clean under ASAN.