From e1b689d34e1758955402cf898ae34a71d5aa69f7 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 18:17:24 -0400 Subject: [PATCH 01/19] Implement ES2024 grouping and resolver builtins --- js/src/builtin/Map.js | 43 +++++++++++ js/src/builtin/MapObject.cpp | 19 ++++- js/src/builtin/MapObject.h | 5 +- js/src/builtin/Object.js | 10 ++- js/src/builtin/Promise.cpp | 43 +++++++++++ js/src/builtin/String.js | 76 +++++++++++++++++++ js/src/jsstr.cpp | 2 + js/src/tests/non262/Promise/with-resolvers.js | 31 ++++++++ js/src/tests/non262/String/well-formed.js | 26 +++++++ js/src/tests/non262/collections/groupBy.js | 34 +++++++++ js/src/vm/SelfHosting.cpp | 17 +++++ 11 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 js/src/tests/non262/Promise/with-resolvers.js create mode 100644 js/src/tests/non262/String/well-formed.js create mode 100644 js/src/tests/non262/collections/groupBy.js diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js index 434cd6529b..bcf0ded02e 100644 --- a/js/src/builtin/Map.js +++ b/js/src/builtin/Map.js @@ -55,6 +55,49 @@ function MapForEach(callbackfn, thisArg = undefined) { } } +// ES2024 +// Map.groupBy ( items, callbackfn ) +function MapGroupBy(items, callbackfn) { + // Step 1. + RequireObjectCoercible(items); + + // Step 2. + if (!IsCallable(callbackfn)) + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, callbackfn)); + + // Step 3. + var groups = std_Map_create(); + + // Step 4. + var k = 0; + + // Steps 5-8. + for (var value of allowContentIter(items)) { + // Step 6.a. + if (k >= MAX_NUMERIC_INDEX) + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + + // Step 6.b. + var key = callContentFunction(callbackfn, undefined, value, k); + + // Steps 6.c-d. + var elements; + if (callFunction(std_Map_has, groups, key)) { + elements = callFunction(std_Map_get, groups, key); + callFunction(std_Array_push, elements, value); + } else { + elements = [value]; + callFunction(std_Map_set, groups, key, elements); + } + + // Step 6.e. + k++; + } + + // Step 9. + return groups; +} + var iteratorTemp = { mapIterationResultPair : null }; function MapIteratorNext() { diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index fe748a6bde..daeb930c86 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -332,9 +332,15 @@ const JSPropertySpec MapObject::staticProperties[] = { JS_PS_END }; +const JSFunctionSpec MapObject::staticMethods[] = { + JS_SELF_HOSTED_FN("groupBy", "MapGroupBy", 2, 0), + JS_FS_END +}; + static JSObject* InitClass(JSContext* cx, Handle global, const Class* clasp, JSProtoKey key, Native construct, const JSPropertySpec* properties, const JSFunctionSpec* methods, + const JSFunctionSpec* staticMethods, const JSPropertySpec* staticProperties) { RootedPlainObject proto(cx, NewBuiltinClassInstance(cx)); @@ -343,8 +349,13 @@ InitClass(JSContext* cx, Handle global, const Class* clasp, JSPro Rooted ctor(cx, global->createConstructor(cx, construct, ClassName(key, cx), 0)); if (!ctor || - !JS_DefineProperties(cx, ctor, staticProperties) || - !LinkConstructorAndPrototype(cx, ctor, proto) || + !JS_DefineProperties(cx, ctor, staticProperties)) + { + return nullptr; + } + if (staticMethods && !JS_DefineFunctions(cx, ctor, staticMethods)) + return nullptr; + if (!LinkConstructorAndPrototype(cx, ctor, proto) || !DefinePropertiesAndFunctions(cx, proto, properties, methods) || !GlobalObject::initBuiltinConstructor(cx, global, key, ctor, proto)) { @@ -359,7 +370,7 @@ MapObject::initClass(JSContext* cx, JSObject* obj) Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Map, construct, properties, methods, - staticProperties)); + staticMethods, staticProperties)); if (proto) { // Define the "entries" method. JSFunction* fun = JS_DefineFunction(cx, proto, "entries", entries, 0, 0); @@ -1084,7 +1095,7 @@ SetObject::initClass(JSContext* cx, JSObject* obj) Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Set, construct, properties, methods, - staticProperties)); + nullptr, staticProperties)); if (proto) { // Define the "values" method. JSFunction* fun = JS_DefineFunction(cx, proto, "values", values, 0, 0); diff --git a/js/src/builtin/MapObject.h b/js/src/builtin/MapObject.h index a9f685ea00..4d58988e3e 100644 --- a/js/src/builtin/MapObject.h +++ b/js/src/builtin/MapObject.h @@ -110,7 +110,9 @@ class MapObject : public NativeObject { static MOZ_MUST_USE bool getKeysAndValuesInterleaved(JSContext* cx, HandleObject obj, JS::MutableHandle> entries); static MOZ_MUST_USE bool entries(JSContext* cx, unsigned argc, Value* vp); + static MOZ_MUST_USE bool get(JSContext* cx, unsigned argc, Value* vp); static MOZ_MUST_USE bool has(JSContext* cx, unsigned argc, Value* vp); + static MOZ_MUST_USE bool set(JSContext* cx, unsigned argc, Value* vp); static MapObject* create(JSContext* cx, HandleObject proto = nullptr); // Publicly exposed Map calls for JSAPI access (webidl maplike/setlike @@ -137,6 +139,7 @@ class MapObject : public NativeObject { static const JSPropertySpec properties[]; static const JSFunctionSpec methods[]; + static const JSFunctionSpec staticMethods[]; static const JSPropertySpec staticProperties[]; ValueMap* getData() { return static_cast(getPrivate()); } static ValueMap& extract(HandleObject o); @@ -153,10 +156,8 @@ class MapObject : public NativeObject { static MOZ_MUST_USE bool size_impl(JSContext* cx, const CallArgs& args); static MOZ_MUST_USE bool size(JSContext* cx, unsigned argc, Value* vp); static MOZ_MUST_USE bool get_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool get(JSContext* cx, unsigned argc, Value* vp); static MOZ_MUST_USE bool has_impl(JSContext* cx, const CallArgs& args); static MOZ_MUST_USE bool set_impl(JSContext* cx, const CallArgs& args); - static MOZ_MUST_USE bool set(JSContext* cx, unsigned argc, Value* vp); static MOZ_MUST_USE bool delete_impl(JSContext* cx, const CallArgs& args); static MOZ_MUST_USE bool delete_(JSContext* cx, unsigned argc, Value* vp); static MOZ_MUST_USE bool keys_impl(JSContext* cx, const CallArgs& args); diff --git a/js/src/builtin/Object.js b/js/src/builtin/Object.js index 6627aea593..01442144cc 100644 --- a/js/src/builtin/Object.js +++ b/js/src/builtin/Object.js @@ -240,12 +240,16 @@ function ObjectGroupBy(items, callbackfn) { // Steps 5-8. for (var value of allowContentIter(items)) { // Step 6.a. - var key = callContentFunction(callbackfn, undefined, value, k); + if (k >= MAX_NUMERIC_INDEX) + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); // Step 6.b. + var key = callContentFunction(callbackfn, undefined, value, k); + + // Step 6.c. key = ToPropertyKey(key); - // Steps 6.c-d. + // Steps 6.d-e. var elements = groups[key]; if (elements === undefined) { _DefineDataProperty(groups, key, [value]); @@ -253,7 +257,7 @@ function ObjectGroupBy(items, callbackfn) { callFunction(std_Array_push, elements, value); } - // Step 6.e. + // Step 6.f. k++; } diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 864d872fb5..231f915ab4 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -3722,6 +3722,48 @@ Promise_static_resolve(JSContext* cx, unsigned argc, Value* vp) return true; } +// ES2024 +// Promise.withResolvers ( ) +static bool +Promise_static_withResolvers(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.thisv().isObject()) { + ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, -1, args.thisv(), nullptr); + return false; + } + RootedObject C(cx, &args.thisv().toObject()); + + // Step 2. + Rooted capability(cx); + if (!NewPromiseCapability(cx, C, &capability, false)) + return false; + + // Step 3. + RootedPlainObject obj(cx, NewBuiltinClassInstance(cx)); + if (!obj) + return false; + + // Steps 4-7. + RootedValue promise(cx, ObjectValue(*capability.promise())); + if (!::JS_DefineProperty(cx, obj, "promise", promise, JSPROP_ENUMERATE)) + return false; + + RootedValue resolve(cx, ObjectValue(*capability.resolve())); + if (!::JS_DefineProperty(cx, obj, "resolve", resolve, JSPROP_ENUMERATE)) + return false; + + RootedValue reject(cx, ObjectValue(*capability.reject())); + if (!::JS_DefineProperty(cx, obj, "reject", reject, JSPROP_ENUMERATE)) + return false; + + // Step 8. + args.rval().setObject(*obj); + return true; +} + /** * Unforgeable version of ES2016, 25.4.4.5, Promise.resolve. */ @@ -5251,6 +5293,7 @@ static const JSFunctionSpec promise_static_methods[] = { JS_FN("race", Promise_static_race, 1, 0), JS_FN("reject", Promise_reject, 1, 0), JS_FN("resolve", Promise_static_resolve, 1, 0), + JS_FN("withResolvers", Promise_static_withResolvers, 0, 0), JS_FS_END }; diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js index e1c32482ae..a83a06ad2b 100644 --- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -654,6 +654,82 @@ function String_repeat(count) { return T; } +// ES2024 +// String.prototype.isWellFormed ( ) +function String_isWellFormed() { + // Steps 1-2. + RequireObjectCoercible(this); + var S = ToString(this); + + // Step 3. + var length = S.length; + for (var k = 0; k < length; k++) { + var c = callFunction(std_String_charCodeAt, S, k); + if (c >= 0xD800 && c <= 0xDBFF) { + if (k + 1 >= length) + return false; + + var d = callFunction(std_String_charCodeAt, S, k + 1); + if (d < 0xDC00 || d > 0xDFFF) + return false; + + k++; + } else if (c >= 0xDC00 && c <= 0xDFFF) { + return false; + } + } + + // Step 4. + return true; +} + +// ES2024 +// String.prototype.toWellFormed ( ) +function String_toWellFormed() { + // Steps 1-2. + RequireObjectCoercible(this); + var S = ToString(this); + + // Step 3. + var length = S.length; + var result = ""; + var copied = 0; + + for (var k = 0; k < length; k++) { + var c = callFunction(std_String_charCodeAt, S, k); + var isUnpairedSurrogate = false; + + if (c >= 0xD800 && c <= 0xDBFF) { + if (k + 1 < length) { + var d = callFunction(std_String_charCodeAt, S, k + 1); + if (d >= 0xDC00 && d <= 0xDFFF) { + k++; + continue; + } + } + isUnpairedSurrogate = true; + } else if (c >= 0xDC00 && c <= 0xDFFF) { + isUnpairedSurrogate = true; + } + + if (isUnpairedSurrogate) { + if (copied < k) + result += callFunction(String_substring, S, copied, k); + result += "\uFFFD"; + copied = k + 1; + } + } + + if (copied === 0) + return S; + + if (copied < length) + result += callFunction(String_substring, S, copied, length); + + // Step 4. + return result; +} + // ES6 draft specification, section 21.1.3.27, version 2013-09-27. function String_iterator() { RequireObjectCoercible(this); diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index 593cf4d708..ee8be97801 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -3305,6 +3305,8 @@ static const JSFunctionSpec string_methods[] = { JS_SELF_HOSTED_FN("toLocaleUpperCase", "String_toLocaleUpperCase", 0,0), JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1,0), JS_SELF_HOSTED_FN("repeat", "String_repeat", 1,0), + JS_SELF_HOSTED_FN("isWellFormed", "String_isWellFormed", 0,0), + JS_SELF_HOSTED_FN("toWellFormed", "String_toWellFormed", 0,0), JS_FN("normalize", str_normalize, 0,0), /* Perl-ish methods (search is actually Python-esque). */ diff --git a/js/src/tests/non262/Promise/with-resolvers.js b/js/src/tests/non262/Promise/with-resolvers.js new file mode 100644 index 0000000000..985d458622 --- /dev/null +++ b/js/src/tests/non262/Promise/with-resolvers.js @@ -0,0 +1,31 @@ +// |reftest| skip-if(!Promise.withResolvers) + +var desc = Object.getOwnPropertyDescriptor(Promise, "withResolvers"); +assertEq(desc.enumerable, false); +assertEq(desc.configurable, true); +assertEq(desc.writable, true); +assertEq(Promise.withResolvers.length, 0); +assertEq(Promise.withResolvers.name, "withResolvers"); + +var capability = Promise.withResolvers(); +assertEq(capability.promise instanceof Promise, true); +assertEq(typeof capability.resolve, "function"); +assertEq(typeof capability.reject, "function"); +assertEqArray(Object.keys(capability), ["promise", "resolve", "reject"]); + +capability.resolve(42); +capability.promise.then(v => assertEq(v, 42)); + +class MyPromise extends Promise {} +var subCapability = Promise.withResolvers.call(MyPromise); +assertEq(subCapability.promise instanceof MyPromise, true); +subCapability.reject("rejected"); +subCapability.promise.then( + () => { throw new Error("expected rejection"); }, + reason => assertEq(reason, "rejected") +); + +assertThrowsInstanceOf(() => Promise.withResolvers.call({}), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/String/well-formed.js b/js/src/tests/non262/String/well-formed.js new file mode 100644 index 0000000000..7472f6afa8 --- /dev/null +++ b/js/src/tests/non262/String/well-formed.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!String.prototype.isWellFormed||!String.prototype.toWellFormed) + +assertEq("".isWellFormed(), true); +assertEq("abc".isWellFormed(), true); +assertEq("\uD83D\uDE00".isWellFormed(), true); +assertEq("\uD800".isWellFormed(), false); +assertEq("\uDC00".isWellFormed(), false); +assertEq("\uD800a".isWellFormed(), false); +assertEq("a\uDC00".isWellFormed(), false); +assertEq("\uD800\uD800\uDC00".isWellFormed(), false); + +assertEq("abc".toWellFormed(), "abc"); +assertEq("\uD83D\uDE00".toWellFormed(), "\uD83D\uDE00"); +assertEq("\uD800".toWellFormed(), "\uFFFD"); +assertEq("\uDC00".toWellFormed(), "\uFFFD"); +assertEq("\uD800a\uDC00".toWellFormed(), "\uFFFDa\uFFFD"); +assertEq("\uD800\uD800\uDC00".toWellFormed(), "\uFFFD\uD800\uDC00"); + +assertEq(String.prototype.isWellFormed.call(123), true); +assertEq(String.prototype.toWellFormed.call({ toString() { return "\uD800x"; } }), "\uFFFDx"); + +assertThrowsInstanceOf(() => String.prototype.isWellFormed.call(null), TypeError); +assertThrowsInstanceOf(() => String.prototype.toWellFormed.call(undefined), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/non262/collections/groupBy.js b/js/src/tests/non262/collections/groupBy.js new file mode 100644 index 0000000000..121a899ede --- /dev/null +++ b/js/src/tests/non262/collections/groupBy.js @@ -0,0 +1,34 @@ +// |reftest| skip-if(!Object.groupBy||!Map.groupBy) + +var objectGroups = Object.groupBy(["a", "bb", "c"], value => value.length); +assertEq(Object.getPrototypeOf(objectGroups), null); +assertEqArray(objectGroups["1"], ["a", "c"]); +assertEqArray(objectGroups["2"], ["bb"]); + +var symbol = Symbol(); +var symbolGroups = Object.groupBy([1, 2], value => value === 1 ? symbol : "__proto__"); +assertEqArray(symbolGroups[symbol], [1]); +assertEqArray(symbolGroups.__proto__, [2]); + +var indexes = []; +var mapGroups = Map.groupBy(["a", "bb", "c"], (value, index) => { + indexes.push(index); + return value.length; +}); +assertEq(mapGroups instanceof Map, true); +assertEqArray(indexes, [0, 1, 2]); +assertEqArray(mapGroups.get(1), ["a", "c"]); +assertEqArray(mapGroups.get(2), ["bb"]); + +var key = {}; +var objectKeyGroups = Map.groupBy([1, 2], value => value === 1 ? key : NaN); +assertEqArray(objectKeyGroups.get(key), [1]); +assertEqArray(objectKeyGroups.get(NaN), [2]); + +assertThrowsInstanceOf(() => Object.groupBy(null, x => x), TypeError); +assertThrowsInstanceOf(() => Map.groupBy(undefined, x => x), TypeError); +assertThrowsInstanceOf(() => Object.groupBy([], null), TypeError); +assertThrowsInstanceOf(() => Map.groupBy([], null), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index c9f290463e..2b2418bbdb 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -270,6 +270,20 @@ intrinsic_GetBuiltinConstructor(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +intrinsic_NewMap(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + Rooted map(cx, MapObject::create(cx)); + if (!map) + return false; + + args.rval().setObject(*map); + return true; +} + static bool intrinsic_SubstringKernel(JSContext* cx, unsigned argc, Value* vp) { @@ -2255,7 +2269,10 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("std_Math_min", math_min, 2,0, MathMin), JS_INLINABLE_FN("std_Math_abs", math_abs, 1,0, MathAbs), + JS_FN("std_Map_create", intrinsic_NewMap, 0,0), + JS_FN("std_Map_get", MapObject::get, 1,0), JS_FN("std_Map_has", MapObject::has, 1,0), + JS_FN("std_Map_set", MapObject::set, 2,0), JS_FN("std_Map_iterator", MapObject::entries, 0,0), JS_FN("std_Number_valueOf", num_valueOf, 0,0), From 3be309faa7e3a599b38e58c0599edf64dd38a18d Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 18:35:44 -0400 Subject: [PATCH 02/19] Implement ES2024 ArrayBuffer transfer APIs --- .../non262/ArrayBuffer/resizable-transfer.js | 68 ++++ js/src/vm/ArrayBufferObject.cpp | 332 +++++++++++++++++- js/src/vm/ArrayBufferObject.h | 37 +- js/src/vm/CommonPropertyNames.h | 1 + 4 files changed, 428 insertions(+), 10 deletions(-) create mode 100644 js/src/tests/non262/ArrayBuffer/resizable-transfer.js diff --git a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js new file mode 100644 index 0000000000..b9f9dbb13a --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js @@ -0,0 +1,68 @@ +// |reftest| skip-if(!ArrayBuffer.prototype.transfer) + +var fixed = new ArrayBuffer(4); +assertEq(fixed.byteLength, 4); +assertEq(fixed.maxByteLength, 4); +assertEq(fixed.resizable, false); +assertEq(fixed.detached, false); +assertThrowsInstanceOf(() => fixed.resize(2), TypeError); +assertEq(ArrayBuffer.prototype.transfer.length, 0); +assertEq(ArrayBuffer.prototype.transferToFixedLength.length, 0); +var resizeArgumentConverted = false; +assertThrowsInstanceOf(() => fixed.resize({ valueOf() { resizeArgumentConverted = true; return 1; } }), + TypeError); +assertEq(resizeArgumentConverted, false); + +var resizable = new ArrayBuffer(4, { maxByteLength: 8 }); +assertEq(resizable.byteLength, 4); +assertEq(resizable.maxByteLength, 8); +assertEq(resizable.resizable, true); + +var bytes = new Uint8Array(resizable); +bytes[0] = 11; +bytes[3] = 44; +resizable.resize(6); +assertEq(resizable.byteLength, 6); +assertEq(new Uint8Array(resizable)[0], 11); +assertEq(new Uint8Array(resizable)[3], 44); +assertEq(new Uint8Array(resizable)[4], 0); +assertThrowsInstanceOf(() => resizable.resize(9), RangeError); + +var source = new ArrayBuffer(4); +var sourceBytes = new Uint8Array(source); +sourceBytes[0] = 1; +sourceBytes[1] = 2; +var sourceView = new Uint8Array(source); +var moved = source.transfer(6); +assertEq(source.detached, true); +assertEq(source.byteLength, 0); +assertEq(source.maxByteLength, 0); +assertEq(sourceView.length, 0); +assertEq(moved.byteLength, 6); +assertEq(moved.resizable, false); +assertEq(moved.maxByteLength, 6); +assertEq(new Uint8Array(moved)[0], 1); +assertEq(new Uint8Array(moved)[1], 2); +assertEq(new Uint8Array(moved)[4], 0); + +var resizableSource = new ArrayBuffer(4, { maxByteLength: 8 }); +new Uint8Array(resizableSource)[0] = 7; +var resizableMoved = resizableSource.transfer(); +assertEq(resizableSource.detached, true); +assertEq(resizableSource.resizable, true); +assertEq(resizableMoved.byteLength, 4); +assertEq(resizableMoved.maxByteLength, 8); +assertEq(resizableMoved.resizable, true); +assertEq(new Uint8Array(resizableMoved)[0], 7); + +var fixedMoved = resizableMoved.transferToFixedLength(10); +assertEq(resizableMoved.detached, true); +assertEq(fixedMoved.byteLength, 10); +assertEq(fixedMoved.maxByteLength, 10); +assertEq(fixedMoved.resizable, false); +assertEq(new Uint8Array(fixedMoved)[0], 7); + +assertThrowsInstanceOf(() => new ArrayBuffer(4, { maxByteLength: 3 }), RangeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 7def8c7e1d..84708d41f0 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -22,6 +22,7 @@ # include #endif +#include #include "jsapi.h" #include "jsarray.h" #include "jscntxt.h" @@ -45,6 +46,7 @@ #include "vm/Interpreter.h" #include "vm/SelfHosting.h" #include "vm/SharedArrayObject.h" +#include "vm/TypedArrayObject.h" #include "vm/WrapperObject.h" #include "wasm/WasmSignalHandlers.h" #include "wasm/WasmTypes.h" @@ -170,12 +172,18 @@ static const JSPropertySpec static_properties[] = { static const JSFunctionSpec prototype_functions[] = { + JS_FN("resize", ArrayBufferObject::fun_resize, 1, 0), JS_SELF_HOSTED_FN("slice", "ArrayBufferSlice", 2, 0), + JS_FN("transfer", ArrayBufferObject::fun_transfer, 0, 0), + JS_FN("transferToFixedLength", ArrayBufferObject::fun_transferToFixedLength, 0, 0), JS_FS_END }; static const JSPropertySpec prototype_properties[] = { JS_PSG("byteLength", ArrayBufferObject::byteLengthGetter, 0), + JS_PSG("detached", ArrayBufferObject::detachedGetter, 0), + JS_PSG("maxByteLength", ArrayBufferObject::maxByteLengthGetter, 0), + JS_PSG("resizable", ArrayBufferObject::resizableGetter, 0), JS_STRING_SYM_PS(toStringTag, "ArrayBuffer", JSPROP_READONLY), JS_PS_END }; @@ -252,6 +260,52 @@ ArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod(cx, args); } +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::detachedGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isDetached()); + return true; +} + +bool +ArrayBufferObject::detachedGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + ArrayBufferObject& buffer = args.thisv().toObject().as(); + args.rval().setInt32(buffer.isDetached() ? 0 : int32_t(buffer.maxByteLength())); + return true; +} + +bool +ArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +ArrayBufferObject::resizableGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isResizable()); + return true; +} + +bool +ArrayBufferObject::resizableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + /* * ArrayBuffer.isView(obj); ES6 (Dec 2013 draft) 24.1.3.1 */ @@ -264,6 +318,39 @@ ArrayBufferObject::fun_isView(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +GetArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options, + uint32_t byteLength, uint32_t* maxByteLength, + bool* resizable) +{ + *maxByteLength = byteLength; + *resizable = false; + + if (!options.isObject()) + return true; + + RootedObject opts(cx, &options.toObject()); + RootedValue maxByteLengthValue(cx); + if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue)) + return false; + + if (maxByteLengthValue.isUndefined()) + return true; + + uint64_t max; + if (!ToIndex(cx, maxByteLengthValue, &max)) + return false; + + if (max > INT32_MAX || max < byteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); + return false; + } + + *maxByteLength = uint32_t(max); + *resizable = true; + return true; +} + // ES2017 draft 24.1.2.1 bool @@ -287,12 +374,22 @@ ArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) } // Step 3. + uint32_t maxByteLength; + bool resizable; + if (!GetArrayBufferMaxByteLengthOption(cx, args.get(1), uint32_t(byteLength), + &maxByteLength, &resizable)) + { + return false; + } + + // Step 4. RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - JSObject* bufobj = create(cx, uint32_t(byteLength), proto); + JSObject* bufobj = create(cx, uint32_t(byteLength), BufferContents::createPlain(nullptr), + OwnsData, proto, GenericObject, maxByteLength, resizable); if (!bufobj) return false; args.rval().setObject(*bufobj); @@ -302,13 +399,57 @@ ArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) static ArrayBufferObject::BufferContents AllocateArrayBufferContents(JSContext* cx, uint32_t nbytes) { - uint8_t* p = cx->runtime()->pod_callocCanGC(nbytes); + uint8_t* p = cx->runtime()->pod_callocCanGC(nbytes ? nbytes : 1); if (!p) ReportOutOfMemory(cx); return ArrayBufferObject::BufferContents::create(p); } +static bool +ReportArrayBufferNotResizable(JSContext* cx) +{ + JS_ReportErrorASCII(cx, "ArrayBuffer is not resizable"); + return false; +} + +static bool +ReportArrayBufferCannotDetach(JSContext* cx) +{ + JS_ReportErrorASCII(cx, "ArrayBuffer cannot be detached"); + return false; +} + +static bool +ReportArrayBufferLengthOutOfRange(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); + return false; +} + +static bool +ArrayBufferViewFits(ArrayBufferViewObject* view, uint32_t newByteLength) +{ + if (view->is()) { + DataViewObject& dataView = view->as(); + uint32_t byteOffset = dataView.byteOffset(); + uint32_t byteLength = dataView.byteLength(); + return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; + } + + if (view->is()) { + TypedArrayObject& typedArray = view->as(); + if (typedArray.isSharedMemory()) + return true; + uint32_t byteOffset = typedArray.byteOffset(); + uint32_t byteLength = typedArray.byteLength(); + return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; + } + + // Outline typed objects don't have a recoverable fixed byte range here. + return false; +} + static void NoteViewBufferWasDetached(ArrayBufferViewObject* view, ArrayBufferObject::BufferContents newContents, @@ -434,6 +575,173 @@ ArrayBufferObject::changeContents(JSContext* cx, BufferContents newContents, changeViewContents(cx, firstView(), oldDataPointer, newContents); } +void +ArrayBufferObject::changeContentsForResize(JSContext* cx, BufferContents newContents, + OwnsState ownsState, uint32_t newByteLength) +{ + MOZ_RELEASE_ASSERT(!isWasm()); + MOZ_ASSERT(!forInlineTypedObject()); + + uint8_t* oldDataPointer = dataPointer(); + setNewData(cx->runtime()->defaultFreeOp(), newContents, ownsState); + + auto& innerViews = cx->compartment()->innerViews.get(); + if (InnerViewTable::ViewVector* views = innerViews.maybeViewsUnbarriered(this)) { + for (size_t i = 0; i < views->length(); i++) { + ArrayBufferViewObject* view = (*views)[i]; + if (ArrayBufferViewFits(view, newByteLength)) + changeViewContents(cx, view, oldDataPointer, newContents); + else + NoteViewBufferWasDetached(view, newContents, cx); + } + } + + if (firstView()) { + if (ArrayBufferViewFits(firstView(), newByteLength)) + changeViewContents(cx, firstView(), oldDataPointer, newContents); + else + NoteViewBufferWasDetached(firstView(), newContents, cx); + } + + setByteLength(newByteLength); +} + +static bool +ResizeArrayBuffer(JSContext* cx, Handle buffer, uint32_t newByteLength) +{ + if (!buffer->isResizable()) + return ReportArrayBufferNotResizable(cx); + + if (buffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + if (newByteLength > buffer->maxByteLength()) + return ReportArrayBufferLengthOutOfRange(cx); + + if (!buffer->isPlain() || buffer->isPreparedForAsmJS() || buffer->forInlineTypedObject()) + return ReportArrayBufferCannotDetach(cx); + + if (newByteLength == buffer->byteLength()) + return true; + + ArrayBufferObject::BufferContents newContents = AllocateArrayBufferContents(cx, newByteLength); + if (!newContents) + return false; + + uint32_t copyLength = std::min(newByteLength, buffer->byteLength()); + if (copyLength > 0) + memcpy(newContents.data(), buffer->dataPointer(), copyLength); + + buffer->changeContentsForResize(cx, newContents, ArrayBufferObject::OwnsData, newByteLength); + return true; +} + +bool +ArrayBufferObject::fun_resize_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + + Rooted buffer(cx, &args.thisv().toObject().as()); + if (!buffer->isResizable()) + return ReportArrayBufferNotResizable(cx); + + uint64_t newByteLength; + if (!ToIndex(cx, args.get(0), &newByteLength)) + return false; + if (newByteLength > INT32_MAX) + return ReportArrayBufferLengthOutOfRange(cx); + + if (!ResizeArrayBuffer(cx, buffer, uint32_t(newByteLength))) + return false; + + args.rval().setUndefined(); + return true; +} + +bool +ArrayBufferObject::fun_resize(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool +ArrayBufferTransfer(JSContext* cx, const CallArgs& args, bool preserveResizability) +{ + MOZ_ASSERT(IsArrayBuffer(args.thisv())); + Rooted buffer(cx, &args.thisv().toObject().as()); + + uint32_t newByteLength = buffer->byteLength(); + if (args.hasDefined(0)) { + uint64_t newLength; + if (!ToIndex(cx, args.get(0), &newLength)) + return false; + if (newLength > INT32_MAX) + return ReportArrayBufferLengthOutOfRange(cx); + newByteLength = uint32_t(newLength); + } + + if (buffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + if (buffer->isWasm() || buffer->isPreparedForAsmJS()) + return ReportArrayBufferCannotDetach(cx); + + bool newResizable = preserveResizability && buffer->isResizable(); + uint32_t newMaxByteLength = newResizable ? buffer->maxByteLength() : newByteLength; + if (newResizable && newByteLength > newMaxByteLength) + return ReportArrayBufferLengthOutOfRange(cx); + + Rooted newBuffer(cx, + ArrayBufferObject::create(cx, newByteLength, ArrayBufferObject::BufferContents::createPlain(nullptr), + ArrayBufferObject::OwnsData, nullptr, GenericObject, + newMaxByteLength, newResizable)); + if (!newBuffer) + return false; + + uint32_t copyLength = std::min(newByteLength, buffer->byteLength()); + if (copyLength > 0) + memcpy(newBuffer->dataPointer(), buffer->dataPointer(), copyLength); + + ArrayBufferObject::BufferContents detachedContents = + buffer->hasStealableContents() ? ArrayBufferObject::BufferContents::createPlain(nullptr) + : buffer->contents(); + ArrayBufferObject::detach(cx, buffer, detachedContents); + + args.rval().setObject(*newBuffer); + return true; +} + +bool +ArrayBufferObject::fun_transfer_impl(JSContext* cx, const CallArgs& args) +{ + return ArrayBufferTransfer(cx, args, true); +} + +bool +ArrayBufferObject::fun_transfer(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +bool +ArrayBufferObject::fun_transferToFixedLength_impl(JSContext* cx, const CallArgs& args) +{ + return ArrayBufferTransfer(cx, args, false); +} + +bool +ArrayBufferObject::fun_transferToFixedLength(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + /* * Wasm Raw Buf Linear Memory Structure * @@ -887,6 +1195,14 @@ ArrayBufferObject::byteLength() const return getSlot(BYTE_LENGTH_SLOT).toInt32(); } +uint32_t +ArrayBufferObject::maxByteLength() const +{ + if (!isResizable()) + return byteLength(); + return getSlot(MAX_BYTE_LENGTH_SLOT).toInt32(); +} + void ArrayBufferObject::setByteLength(uint32_t length) { @@ -1035,9 +1351,12 @@ ArrayBufferObject* ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents contents, OwnsState ownsState /* = OwnsData */, HandleObject proto /* = nullptr */, - NewObjectKind newKind /* = GenericObject */) + NewObjectKind newKind /* = GenericObject */, + uint32_t maxByteLength /* = 0 */, + bool resizable /* = false */) { MOZ_ASSERT_IF(contents.kind() == MAPPED, contents); + MOZ_ASSERT_IF(resizable, maxByteLength >= nbytes); // 24.1.1.1, step 3 (Inlined 6.2.6.1 CreateByteDataBlock, step 2). // Refuse to allocate too large buffers, currently limited to ~2 GiB. @@ -1068,7 +1387,7 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content MOZ_ASSERT(ownsState == OwnsData); size_t usableSlots = NativeObject::MAX_FIXED_SLOTS - reservedSlots; if (nbytes <= usableSlots * sizeof(Value)) { - int newSlots = (nbytes - 1) / sizeof(Value) + 1; + int newSlots = nbytes == 0 ? 0 : (nbytes - 1) / sizeof(Value) + 1; MOZ_ASSERT(int(nbytes) <= newSlots * int(sizeof(Value))); nslots = reservedSlots + newSlots; contents = BufferContents::createPlain(nullptr); @@ -1098,9 +1417,10 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content if (!contents) { void* data = obj->inlineDataPointer(); memset(data, 0, nbytes); - obj->initialize(nbytes, BufferContents::createPlain(data), DoesntOwnData); + obj->initialize(nbytes, BufferContents::createPlain(data), DoesntOwnData, + maxByteLength, resizable); } else { - obj->initialize(nbytes, contents, ownsState); + obj->initialize(nbytes, contents, ownsState, maxByteLength, resizable); } return obj; diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index f4010c6c77..d0d83c431d 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -128,15 +128,22 @@ typedef MutableHandle MutableHandleArrayBufferObj class ArrayBufferObject : public ArrayBufferObjectMaybeShared { static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool resizableGetterImpl(JSContext* cx, const CallArgs& args); + static bool detachedGetterImpl(JSContext* cx, const CallArgs& args); static bool fun_slice_impl(JSContext* cx, const CallArgs& args); + static bool fun_resize_impl(JSContext* cx, const CallArgs& args); + static bool fun_transfer_impl(JSContext* cx, const CallArgs& args); + static bool fun_transferToFixedLength_impl(JSContext* cx, const CallArgs& args); public: static const uint8_t DATA_SLOT = 0; static const uint8_t BYTE_LENGTH_SLOT = 1; static const uint8_t FIRST_VIEW_SLOT = 2; static const uint8_t FLAGS_SLOT = 3; + static const uint8_t MAX_BYTE_LENGTH_SLOT = 4; - static const uint8_t RESERVED_SLOTS = 4; + static const uint8_t RESERVED_SLOTS = 5; static const size_t ARRAY_BUFFER_ALIGNMENT = 8; @@ -189,7 +196,11 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared // This PLAIN or WASM buffer has been prepared for asm.js and cannot // henceforth be transferred/detached. - FOR_ASMJS = 0x40 + FOR_ASMJS = 0x40, + + // This buffer was created with [[ArrayBufferMaxByteLength]] and can + // be resized up to that maximum. + RESIZABLE = 0x80 }; static_assert(JS_ARRAYBUFFER_DETACHED_FLAG == DETACHED, @@ -230,8 +241,14 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared static const Class class_; static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool resizableGetter(JSContext* cx, unsigned argc, Value* vp); + static bool detachedGetter(JSContext* cx, unsigned argc, Value* vp); static bool fun_slice(JSContext* cx, unsigned argc, Value* vp); + static bool fun_resize(JSContext* cx, unsigned argc, Value* vp); + static bool fun_transfer(JSContext* cx, unsigned argc, Value* vp); + static bool fun_transferToFixedLength(JSContext* cx, unsigned argc, Value* vp); static bool fun_isView(JSContext* cx, unsigned argc, Value* vp); @@ -243,7 +260,9 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared BufferContents contents, OwnsState ownsState = OwnsData, HandleObject proto = nullptr, - NewObjectKind newKind = GenericObject); + NewObjectKind newKind = GenericObject, + uint32_t maxByteLength = 0, + bool resizable = false); static ArrayBufferObject* create(JSContext* cx, uint32_t nbytes, HandleObject proto = nullptr, NewObjectKind newKind = GenericObject); @@ -294,6 +313,8 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void setNewData(FreeOp* fop, BufferContents newContents, OwnsState ownsState); void changeContents(JSContext* cx, BufferContents newContents, OwnsState ownsState); + void changeContentsForResize(JSContext* cx, BufferContents newContents, + OwnsState ownsState, uint32_t newByteLength); // Detach this buffer from its original memory. (This necessarily makes // views of this buffer unusable for modifying that original memory.) @@ -311,6 +332,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared uint8_t* dataPointer() const; SharedMem dataPointerShared() const; uint32_t byteLength() const; + uint32_t maxByteLength() const; BufferContents contents() const { return BufferContents(dataPointer(), bufferKind()); @@ -334,6 +356,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared bool isWasm() const { return bufferKind() == WASM; } bool isMapped() const { return bufferKind() == MAPPED; } bool isDetached() const { return flags() & DETACHED; } + bool isResizable() const { return flags() & RESIZABLE; } bool isPreparedForAsmJS() const { return flags() & FOR_ASMJS; } // WebAssembly support: @@ -391,12 +414,17 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void setIsDetached() { setFlags(flags() | DETACHED); } void setIsPreparedForAsmJS() { setFlags(flags() | FOR_ASMJS); } + void setIsResizable() { setFlags(flags() | RESIZABLE); } - void initialize(size_t byteLength, BufferContents contents, OwnsState ownsState) { + void initialize(size_t byteLength, BufferContents contents, OwnsState ownsState, + uint32_t maxByteLength = 0, bool resizable = false) { setByteLength(byteLength); setFlags(0); + setFixedSlot(MAX_BYTE_LENGTH_SLOT, Int32Value(maxByteLength ? maxByteLength : byteLength)); setFirstView(nullptr); setDataPointer(contents, ownsState); + if (resizable) + setIsResizable(); } // Note: initialize() may be called after initEmpty(); initEmpty() must @@ -404,6 +432,7 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared void initEmpty() { setByteLength(0); setFlags(0); + setFixedSlot(MAX_BYTE_LENGTH_SLOT, Int32Value(0)); setFirstView(nullptr); setDataPointer(BufferContents::createPlain(nullptr), DoesntOwnData); } diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 64bab961cd..39a5d7727a 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -248,6 +248,7 @@ macro(lookupSetter, lookupSetter, "__lookupSetter__") \ macro(MapConstructorInit, MapConstructorInit, "MapConstructorInit") \ macro(MapIterator, MapIterator, "Map Iterator") \ + macro(maxByteLength, maxByteLength, "maxByteLength") \ macro(maximumFractionDigits, maximumFractionDigits, "maximumFractionDigits") \ macro(maximumSignificantDigits, maximumSignificantDigits, "maximumSignificantDigits") \ macro(message, message, "message") \ From e317bf10fcb9feabc35075f7f521abb78a5d53c0 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 18:48:12 -0400 Subject: [PATCH 03/19] Allow symbols as weak collection keys --- js/src/builtin/WeakMapObject.cpp | 223 ++++++++++++++++----- js/src/builtin/WeakMapObject.h | 6 +- js/src/builtin/WeakSet.js | 6 +- js/src/builtin/WeakSetObject.cpp | 7 +- js/src/jsweakmap.h | 38 ++++ js/src/tests/non262/WeakMap/symbol-keys.js | 36 ++++ js/src/vm/SelfHosting.cpp | 10 + 7 files changed, 266 insertions(+), 60 deletions(-) create mode 100644 js/src/tests/non262/WeakMap/symbol-keys.js diff --git a/js/src/builtin/WeakMapObject.cpp b/js/src/builtin/WeakMapObject.cpp index 82b6ef1a7c..d68b9e05fd 100644 --- a/js/src/builtin/WeakMapObject.cpp +++ b/js/src/builtin/WeakMapObject.cpp @@ -8,6 +8,7 @@ #include "jsapi.h" #include "jscntxt.h" +#include "builtin/WeakRefObject.h" #include "vm/SelfHosting.h" #include "vm/Interpreter-inl.h" @@ -21,21 +22,80 @@ IsWeakMap(HandleValue v) return v.isObject() && v.toObject().is(); } +struct WeakMapObject::Data +{ + ObjectValueMap objectMap; + SymbolValueMap symbolMap; + + Data(JSContext* cx, JSObject* owner) + : objectMap(cx, owner), + symbolMap(cx, owner) + {} + + bool init() { + return objectMap.init() && symbolMap.init(); + } +}; + +ObjectValueMap* +WeakMapObject::getMap() +{ + Data* data = getData(); + return data ? &data->objectMap : nullptr; +} + +SymbolValueMap* +WeakMapObject::getSymbolMap() +{ + Data* data = getData(); + return data ? &data->symbolMap : nullptr; +} + +static bool +EnsureWeakMapData(JSContext* cx, Handle mapObj) +{ + if (mapObj->getData()) + return true; + + auto data = cx->make_unique(cx, mapObj.get()); + if (!data) + return false; + + if (!data->init()) { + JS_ReportOutOfMemory(cx); + return false; + } + + mapObj->setPrivate(data.release()); + return true; +} + MOZ_ALWAYS_INLINE bool WeakMap_has_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setBoolean(false); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (map->has(key)) { - args.rval().setBoolean(true); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (map->has(key)) { + args.rval().setBoolean(true); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (map->has(key)) { + args.rval().setBoolean(true); + return true; + } } } @@ -55,16 +115,27 @@ WeakMap_get_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setUndefined(); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (ObjectValueMap::Ptr ptr = map->lookup(key)) { - args.rval().set(ptr->value()); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (ObjectValueMap::Ptr ptr = map->lookup(key)) { + args.rval().set(ptr->value()); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (SymbolValueMap::Ptr ptr = map->lookup(key)) { + args.rval().set(ptr->value()); + return true; + } } } @@ -84,17 +155,29 @@ WeakMap_delete_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsWeakMap(args.thisv())); - if (!args.get(0).isObject()) { + if (!CanBeHeldWeakly(args.get(0))) { args.rval().setBoolean(false); return true; } - if (ObjectValueMap* map = args.thisv().toObject().as().getMap()) { - JSObject* key = &args[0].toObject(); - if (ObjectValueMap::Ptr ptr = map->lookup(key)) { - map->remove(ptr); - args.rval().setBoolean(true); - return true; + WeakMapObject& weakMap = args.thisv().toObject().as(); + if (args.get(0).isObject()) { + if (ObjectValueMap* map = weakMap.getMap()) { + JSObject* key = &args[0].toObject(); + if (ObjectValueMap::Ptr ptr = map->lookup(key)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } + } + } else { + if (SymbolValueMap* map = weakMap.getSymbolMap()) { + JS::Symbol* key = args[0].toSymbol(); + if (SymbolValueMap::Ptr ptr = map->lookup(key)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } } } @@ -126,22 +209,13 @@ TryPreserveReflector(JSContext* cx, HandleObject obj) return true; } -static MOZ_ALWAYS_INLINE bool -SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, - HandleObject key, HandleValue value) +static bool +SetWeakMapObjectEntryInternal(JSContext* cx, Handle mapObj, + HandleObject key, HandleValue value) { + if (!EnsureWeakMapData(cx, mapObj)) + return false; ObjectValueMap* map = mapObj->getMap(); - if (!map) { - auto newMap = cx->make_unique(cx, mapObj.get()); - if (!newMap) - return false; - if (!newMap->init()) { - JS_ReportOutOfMemory(cx); - return false; - } - map = newMap.release(); - mapObj->setPrivate(map); - } // Preserve wrapped native keys to prevent wrapper optimization. if (!TryPreserveReflector(cx, key)) @@ -162,13 +236,28 @@ SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, return true; } -MOZ_ALWAYS_INLINE bool -WeakMap_set_impl(JSContext* cx, const CallArgs& args) +static bool +SetWeakMapSymbolEntryInternal(JSContext* cx, Handle mapObj, + Handle key, HandleValue value) { - MOZ_ASSERT(IsWeakMap(args.thisv())); + if (!EnsureWeakMapData(cx, mapObj)) + return false; + SymbolValueMap* map = mapObj->getSymbolMap(); - if (!args.get(0).isObject()) { - UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, args.get(0), nullptr); + MOZ_ASSERT_IF(value.isObject(), value.toObject().compartment() == mapObj->compartment()); + if (!map->put(key, value)) { + JS_ReportOutOfMemory(cx); + return false; + } + return true; +} + +static MOZ_ALWAYS_INLINE bool +SetWeakMapEntryInternal(JSContext* cx, Handle mapObj, + HandleValue key, HandleValue value) +{ + if (!CanBeHeldWeakly(key)) { + UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, key, nullptr); if (!bytes) return false; JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, @@ -176,11 +265,24 @@ WeakMap_set_impl(JSContext* cx, const CallArgs& args) return false; } - RootedObject key(cx, &args[0].toObject()); + if (key.isObject()) { + RootedObject objectKey(cx, &key.toObject()); + return SetWeakMapObjectEntryInternal(cx, mapObj, objectKey, value); + } + + Rooted symbolKey(cx, key.toSymbol()); + return SetWeakMapSymbolEntryInternal(cx, mapObj, symbolKey, value); +} + +MOZ_ALWAYS_INLINE bool +WeakMap_set_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsWeakMap(args.thisv())); + Rooted thisObj(cx, &args.thisv().toObject()); Rooted map(cx, &thisObj->as()); - if (!SetWeakMapEntryInternal(cx, map, key, args.get(1))) + if (!SetWeakMapEntryInternal(cx, map, args.get(0), args.get(1))) return false; args.rval().set(args.thisv()); return true; @@ -193,6 +295,13 @@ js::WeakMap_set(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod(cx, args); } +bool +js::SetWeakMapEntryValue(JSContext* cx, HandleObject mapObj, HandleValue key, HandleValue val) +{ + Rooted rootedMap(cx, &mapObj->as()); + return SetWeakMapEntryInternal(cx, rootedMap, key, val); +} + JS_FRIEND_API(bool) JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHandleObject ret) { @@ -205,11 +314,11 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan RootedObject arr(cx, NewDenseEmptyArray(cx)); if (!arr) return false; - ObjectValueMap* map = obj->as().getMap(); - if (map) { + WeakMapObject::Data* data = obj->as().getData(); + if (data) { // Prevent GC from mutating the weakmap while iterating. AutoSuppressGC suppress(cx); - for (ObjectValueMap::Base::Range r = map->all(); !r.empty(); r.popFront()) { + for (ObjectValueMap::Base::Range r = data->objectMap.all(); !r.empty(); r.popFront()) { JS::ExposeObjectToActiveJS(r.front().key()); RootedObject key(cx, r.front().key()); if (!cx->compartment()->wrap(cx, &key)) @@ -217,6 +326,14 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan if (!NewbornArrayPush(cx, arr, ObjectValue(*key))) return false; } + for (SymbolValueMap::Base::Range r = data->symbolMap.all(); !r.empty(); r.popFront()) { + gc::ExposeGCThingToActiveJS(JS::GCCellPtr(r.front().key().get())); + RootedValue key(cx, SymbolValue(r.front().key())); + if (!cx->compartment()->wrap(cx, &key)) + return false; + if (!NewbornArrayPush(cx, arr, key)) + return false; + } } ret.set(arr); return true; @@ -225,21 +342,23 @@ JS_NondeterministicGetWeakMapKeys(JSContext* cx, HandleObject objArg, MutableHan static void WeakMap_mark(JSTracer* trc, JSObject* obj) { - if (ObjectValueMap* map = obj->as().getMap()) - map->trace(trc); + if (WeakMapObject::Data* data = obj->as().getData()) { + data->objectMap.trace(trc); + data->symbolMap.trace(trc); + } } static void WeakMap_finalize(FreeOp* fop, JSObject* obj) { MOZ_ASSERT(fop->maybeOffMainThread()); - if (ObjectValueMap* map = obj->as().getMap()) { + if (WeakMapObject::Data* data = obj->as().getData()) { #ifdef DEBUG - map->~ObjectValueMap(); - memset(static_cast(map), 0xdc, sizeof(*map)); - fop->free_(map); + data->~Data(); + memset(static_cast(data), 0xdc, sizeof(*data)); + fop->free_(data); #else - fop->delete_(map); + fop->delete_(data); #endif } } @@ -282,7 +401,8 @@ JS::SetWeakMapEntry(JSContext* cx, HandleObject mapObj, HandleObject key, CHECK_REQUEST(cx); assertSameCompartment(cx, key, val); Rooted rootedMap(cx, &mapObj->as()); - return SetWeakMapEntryInternal(cx, rootedMap, key, val); + RootedValue keyValue(cx, ObjectValue(*key)); + return SetWeakMapEntryInternal(cx, rootedMap, keyValue, val); } static bool @@ -386,4 +506,3 @@ js::InitBareWeakMapCtor(JSContext* cx, HandleObject obj) { return InitWeakMapClass(cx, obj, false); } - diff --git a/js/src/builtin/WeakMapObject.h b/js/src/builtin/WeakMapObject.h index 507758d2fa..f19ad1d4df 100644 --- a/js/src/builtin/WeakMapObject.h +++ b/js/src/builtin/WeakMapObject.h @@ -16,7 +16,11 @@ class WeakMapObject : public NativeObject public: static const Class class_; - ObjectValueMap* getMap() { return static_cast(getPrivate()); } + struct Data; + + Data* getData() { return static_cast(getPrivate()); } + ObjectValueMap* getMap(); + SymbolValueMap* getSymbolMap(); }; } // namespace js diff --git a/js/src/builtin/WeakSet.js b/js/src/builtin/WeakSet.js index b16b4634dd..88f7929c72 100644 --- a/js/src/builtin/WeakSet.js +++ b/js/src/builtin/WeakSet.js @@ -32,7 +32,7 @@ function WeakSet_add(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "add", typeof S); // Step 5. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, DecompileArg(0, value)); // Steps 7-8. @@ -55,7 +55,7 @@ function WeakSet_delete(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "delete", typeof S); // Step 5. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) return false; // Steps 7-8. @@ -75,7 +75,7 @@ function WeakSet_has(value) { ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "WeakSet", "has", typeof S); // Step 6. - if (!IsObject(value)) + if (!CanBeHeldWeakly(value)) return false; // Steps 7-8. diff --git a/js/src/builtin/WeakSetObject.cpp b/js/src/builtin/WeakSetObject.cpp index bb9708f6f9..055f6b1abd 100644 --- a/js/src/builtin/WeakSetObject.cpp +++ b/js/src/builtin/WeakSetObject.cpp @@ -12,6 +12,7 @@ #include "builtin/MapObject.h" #include "builtin/SelfHostingDefines.h" #include "builtin/WeakMapObject.h" +#include "builtin/WeakRefObject.h" #include "vm/GlobalObject.h" #include "vm/SelfHosting.h" @@ -108,7 +109,6 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) if (optimized) { RootedValue keyVal(cx); - RootedObject keyObject(cx); RootedValue placeholder(cx, BooleanValue(true)); RootedObject map(cx, &obj->getReservedSlot(WEAKSET_MAP_SLOT).toObject()); RootedArrayObject array(cx, &iterable.toObject().as()); @@ -116,7 +116,7 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) keyVal.set(array->getDenseElement(index)); MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); - if (keyVal.isPrimitive()) { + if (!CanBeHeldWeakly(keyVal)) { UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, keyVal, nullptr); if (!bytes) @@ -126,8 +126,7 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) return false; } - keyObject = &keyVal.toObject(); - if (!SetWeakMapEntry(cx, map, keyObject, placeholder)) + if (!SetWeakMapEntryValue(cx, map, keyVal, placeholder)) return false; } } else { diff --git a/js/src/jsweakmap.h b/js/src/jsweakmap.h index c13baa7825..ec0fd8ddc6 100644 --- a/js/src/jsweakmap.h +++ b/js/src/jsweakmap.h @@ -16,11 +16,25 @@ #include "gc/Marking.h" #include "gc/StoreBuffer.h" #include "js/HashTable.h" +#include "vm/Symbol.h" namespace js { class WeakMapBase; +template <> +struct MovableCellHasher +{ + using Key = JS::Symbol*; + using Lookup = JS::Symbol*; + + static bool hasHash(const Lookup& l) { return true; } + static bool ensureHash(const Lookup& l) { return true; } + static HashNumber hash(const Lookup& l) { return l->hash(); } + static bool match(const Key& k, const Lookup& l) { return k == l; } + static void rekey(Key& k, const Key& newKey) { k = newKey; } +}; + // A subclass template of js::HashMap whose keys and values may be garbage-collected. When // a key is collected, the table entry disappears, dropping its reference to the value. // @@ -296,9 +310,16 @@ class WeakMap : public HashMap, return nullptr; } + JSObject* getDelegate(JS::Symbol* sym) const { + return nullptr; + } + private: void exposeGCThingToActiveJS(const JS::Value& v) const { JS::ExposeValueToActiveJS(v); } void exposeGCThingToActiveJS(JSObject* obj) const { JS::ExposeObjectToActiveJS(obj); } + void exposeGCThingToActiveJS(JS::Symbol* sym) const { + gc::ExposeGCThingToActiveJS(JS::GCCellPtr(sym)); + } bool keyNeedsMark(JSObject* key) const { JSObject* delegate = getDelegate(key); @@ -313,6 +334,10 @@ class WeakMap : public HashMap, return false; } + bool keyNeedsMark(JS::Symbol* sym) const { + return false; + } + bool findZoneEdges() override { // This is overridden by ObjectValueMap. return true; @@ -378,6 +403,9 @@ WeakMap_set(JSContext* cx, unsigned argc, Value* vp); extern bool WeakMap_delete(JSContext* cx, unsigned argc, Value* vp); +extern bool +SetWeakMapEntryValue(JSContext* cx, HandleObject mapObj, HandleValue key, HandleValue val); + extern JSObject* InitWeakMapClass(JSContext* cx, HandleObject obj); @@ -394,6 +422,16 @@ class ObjectValueMap : public WeakMap, HeapPtr, virtual bool findZoneEdges(); }; +class SymbolValueMap : public WeakMap, HeapPtr, + MovableCellHasher>> +{ + public: + SymbolValueMap(JSContext* cx, JSObject* obj) + : WeakMap, HeapPtr, + MovableCellHasher>>(cx, obj) + {} +}; + // Generic weak map for mapping objects to other objects. class ObjectWeakMap diff --git a/js/src/tests/non262/WeakMap/symbol-keys.js b/js/src/tests/non262/WeakMap/symbol-keys.js new file mode 100644 index 0000000000..0edb542974 --- /dev/null +++ b/js/src/tests/non262/WeakMap/symbol-keys.js @@ -0,0 +1,36 @@ +var key = Symbol("weak"); +var map = new WeakMap(); +assertEq(map.has(key), false); +assertEq(map.get(key), undefined); +assertEq(map.set(key, 13), map); +assertEq(map.has(key), true); +assertEq(map.get(key), 13); +assertEq(map.delete(key), true); +assertEq(map.has(key), false); + +var constructedKey = Symbol("constructed"); +var constructed = new WeakMap([[constructedKey, 7]]); +assertEq(constructed.get(constructedKey), 7); + +var registered = Symbol.for("registered"); +assertEq(map.has(registered), false); +assertEq(map.get(registered), undefined); +assertEq(map.delete(registered), false); +assertThrowsInstanceOf(() => map.set(registered, 1), TypeError); +assertThrowsInstanceOf(() => new WeakMap([[registered, 1]]), TypeError); + +var setKey = Symbol("set"); +var set = new WeakSet([setKey]); +assertEq(set.has(setKey), true); +assertEq(set.delete(setKey), true); +assertEq(set.has(setKey), false); +assertEq(set.add(setKey), set); +assertEq(set.has(setKey), true); + +assertEq(set.has(registered), false); +assertEq(set.delete(registered), false); +assertThrowsInstanceOf(() => set.add(registered), TypeError); +assertThrowsInstanceOf(() => new WeakSet([registered]), TypeError); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 2b2418bbdb..804e231a3f 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -38,6 +38,7 @@ #include "builtin/SelfHostingDefines.h" #include "builtin/Stream.h" #include "builtin/TypedObject.h" +#include "builtin/WeakRefObject.h" #include "builtin/WeakSetObject.h" #include "gc/Marking.h" #include "gc/Policy.h" @@ -105,6 +106,14 @@ intrinsic_IsObject(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +intrinsic_CanBeHeldWeakly(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(CanBeHeldWeakly(args.get(0))); + return true; +} + static bool intrinsic_IsArray(JSContext* cx, unsigned argc, Value* vp) { @@ -2320,6 +2329,7 @@ static const JSFunctionSpec intrinsic_functions[] = { // Helper funtions after this point. JS_INLINABLE_FN("ToObject", intrinsic_ToObject, 1,0, IntrinsicToObject), JS_INLINABLE_FN("IsObject", intrinsic_IsObject, 1,0, IntrinsicIsObject), + JS_FN("CanBeHeldWeakly", intrinsic_CanBeHeldWeakly, 1,0), JS_INLINABLE_FN("IsArray", intrinsic_IsArray, 1,0, ArrayIsArray), JS_INLINABLE_FN("IsWrappedArrayConstructor", intrinsic_IsWrappedArrayConstructor, 1,0, IntrinsicIsWrappedArrayConstructor), From 2e51dc9f0920b515f72c349589d26c7c8833a5a3 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 19:02:03 -0400 Subject: [PATCH 04/19] Implement growable SharedArrayBuffer --- js/src/js.msg | 1 + .../non262/SharedArrayBuffer/growable.js | 40 ++++ js/src/vm/ArrayBufferObject.cpp | 2 +- js/src/vm/SharedArrayObject.cpp | 171 ++++++++++++++++-- js/src/vm/SharedArrayObject.h | 51 +++++- 5 files changed, 246 insertions(+), 19 deletions(-) create mode 100644 js/src/tests/non262/SharedArrayBuffer/growable.js diff --git a/js/src/js.msg b/js/src/js.msg index 499e7a1623..d9a6c98764 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -561,6 +561,7 @@ MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray // Shared array buffer MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range") +MSG_DEF(JSMSG_SHARED_ARRAY_NOT_GROWABLE, 0, JSEXN_TYPEERR, "SharedArrayBuffer is not growable") MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer") MSG_DEF(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different SharedArrayBuffer, but species constructor returned same SharedArrayBuffer") MSG_DEF(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected SharedArrayBuffer with at least {0} bytes, but species constructor returns SharedArrayBuffer with {1} bytes") diff --git a/js/src/tests/non262/SharedArrayBuffer/growable.js b/js/src/tests/non262/SharedArrayBuffer/growable.js new file mode 100644 index 0000000000..7b9ce85ffe --- /dev/null +++ b/js/src/tests/non262/SharedArrayBuffer/growable.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!this.SharedArrayBuffer) + +if (typeof SharedArrayBuffer === "function") { + const fixed = new SharedArrayBuffer(4); + assertEq(fixed.byteLength, 4); + assertEq(fixed.maxByteLength, 4); + assertEq(fixed.growable, false); + assertThrowsInstanceOf(() => fixed.grow(4), TypeError); + + assertThrowsInstanceOf(() => new SharedArrayBuffer(-1), RangeError); + assertThrowsInstanceOf(() => new SharedArrayBuffer(8, {maxByteLength: 4}), RangeError); + + let optionGetterCalled = false; + const growable = new SharedArrayBuffer(4, { + get maxByteLength() { + optionGetterCalled = true; + return 16; + } + }); + assertEq(optionGetterCalled, true); + assertEq(growable.byteLength, 4); + assertEq(growable.maxByteLength, 16); + assertEq(growable.growable, true); + + const before = new Uint8Array(growable); + before[0] = 37; + assertEq(growable.grow(12), undefined); + assertEq(growable.byteLength, 12); + assertEq(growable.maxByteLength, 16); + + const after = new Uint8Array(growable); + assertEq(after.length, 12); + assertEq(after[0], 37); + + assertThrowsInstanceOf(() => growable.grow(11), RangeError); + assertThrowsInstanceOf(() => growable.grow(17), RangeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 84708d41f0..21314e7134 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -1245,7 +1245,7 @@ js::WasmArrayBufferMaxSize(const ArrayBufferObjectMaybeShared* buf) if (buf->is()) return buf->as().wasmMaxSize(); - return Some(buf->as().byteLength()); + return Some(buf->as().maxByteLength()); } /* static */ bool diff --git a/js/src/vm/SharedArrayObject.cpp b/js/src/vm/SharedArrayObject.cpp index 6a3c6a91c3..a597564695 100644 --- a/js/src/vm/SharedArrayObject.cpp +++ b/js/src/vm/SharedArrayObject.cpp @@ -8,6 +8,7 @@ #include "mozilla/Atomics.h" #include "jsfriendapi.h" +#include "jsnum.h" #include "jsprf.h" #ifdef XP_WIN @@ -104,15 +105,18 @@ SharedArrayAllocSize(uint32_t length) } SharedArrayRawBuffer* -SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) +SharedArrayRawBuffer::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable) { // The value (uint32_t)-1 is used as a signal in various places, // so guard against it on principle. MOZ_ASSERT(length != (uint32_t)-1); + MOZ_ASSERT(maxLength != (uint32_t)-1); + MOZ_ASSERT(maxLength >= length); // Add a page for the header and round to a page boundary. - uint32_t allocSize = SharedArrayAllocSize(length); - if (allocSize <= length) + uint32_t allocationLength = growable ? maxLength : length; + uint32_t allocSize = SharedArrayAllocSize(allocationLength); + if (allocSize <= allocationLength) return nullptr; // Test >= to guard against the case where multiple extant runtimes @@ -127,7 +131,8 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) } } - bool preparedForAsmJS = jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); + bool preparedForAsmJS = + !growable && jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); void* p = nullptr; if (preparedForAsmJS) { @@ -161,8 +166,9 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) uint8_t* buffer = reinterpret_cast(p) + gc::SystemPageSize(); uint8_t* base = buffer - sizeof(SharedArrayRawBuffer); - SharedArrayRawBuffer* rawbuf = new (base) SharedArrayRawBuffer(buffer, length, preparedForAsmJS); - MOZ_ASSERT(rawbuf->length == length); // Deallocation needs this + SharedArrayRawBuffer* rawbuf = + new (base) SharedArrayRawBuffer(buffer, length, maxLength, growable, preparedForAsmJS); + MOZ_ASSERT(rawbuf->allocatedByteLength() == allocationLength); // Deallocation needs this. return rawbuf; } @@ -201,7 +207,7 @@ SharedArrayRawBuffer::dropReference() MOZ_ASSERT(p.asValue() % gc::SystemPageSize() == 0); uint8_t* address = p.unwrap(/*safe - only reference*/); - uint32_t allocSize = SharedArrayAllocSize(this->length); + uint32_t allocSize = SharedArrayAllocSize(this->allocatedByteLength()); if (this->preparedForAsmJS) { uint32_t mappedSize = SharedArrayMappedSize(allocSize); @@ -221,6 +227,23 @@ SharedArrayRawBuffer::dropReference() numLive--; } +bool +SharedArrayRawBuffer::growTo(uint32_t newLength) +{ + MOZ_ASSERT(growable); + MOZ_ASSERT(newLength <= maxLength); + + for (;;) { + uint32_t oldLength = length; + if (newLength < oldLength) + return false; + if (newLength == oldLength) + return true; + if (length.compareExchange(oldLength, newLength)) + return true; + } +} + MOZ_ALWAYS_INLINE bool SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args) @@ -237,6 +260,112 @@ SharedArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* v return CallNonGenericMethod(cx, args); } +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setInt32(args.thisv().toObject().as().maxByteLength()); + return true; +} + +bool +SharedArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::growableGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isGrowable()); + return true; +} + +bool +SharedArrayBufferObject::growableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool +ReportSharedArrayBufferNotGrowable(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_NOT_GROWABLE); + return false; +} + +static bool +ReportSharedArrayBufferLengthOutOfRange(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + return false; +} + +static bool +GetSharedArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options, + uint32_t byteLength, uint32_t* maxByteLength, + bool* growable) +{ + *maxByteLength = byteLength; + *growable = false; + + if (!options.isObject()) + return true; + + RootedObject opts(cx, &options.toObject()); + RootedValue maxByteLengthValue(cx); + if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue)) + return false; + + if (maxByteLengthValue.isUndefined()) + return true; + + uint64_t max; + if (!ToIndex(cx, maxByteLengthValue, &max)) + return false; + + if (max > INT32_MAX || max < byteLength) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + *maxByteLength = uint32_t(max); + *growable = true; + return true; +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::fun_grow_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + + Rooted buffer(cx, + &args.thisv().toObject().as()); + if (!buffer->isGrowable()) + return ReportSharedArrayBufferNotGrowable(cx); + + uint64_t newByteLength; + if (!ToIndex(cx, args.get(0), &newByteLength)) + return false; + if (newByteLength > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + uint32_t newLength = uint32_t(newByteLength); + if (newLength > buffer->maxByteLength() || !buffer->growTo(newLength)) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + args.rval().setUndefined(); + return true; +} + +bool +SharedArrayBufferObject::fun_grow(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + bool SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) { @@ -245,11 +374,19 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!ThrowIfNotConstructing(cx, args, "SharedArrayBuffer")) return false; + uint64_t length64; + if (!ToIndex(cx, args.get(0), &length64)) + return false; // Bugs 1068458, 1161298: Limit length to 2^31-1. - uint32_t length; - bool overflow_unused; - if (!ToLengthClamped(cx, args.get(0), &length, &overflow_unused) || length > INT32_MAX) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + if (length64 > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + uint32_t length = uint32_t(length64); + + uint32_t maxByteLength; + bool growable; + if (!GetSharedArrayBufferMaxByteLengthOption(cx, args.get(1), length, + &maxByteLength, &growable)) + { return false; } @@ -258,7 +395,7 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - JSObject* bufobj = New(cx, length, proto); + JSObject* bufobj = New(cx, length, maxByteLength, growable, proto); if (!bufobj) return false; args.rval().setObject(*bufobj); @@ -266,9 +403,10 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* } SharedArrayBufferObject* -SharedArrayBufferObject::New(JSContext* cx, uint32_t length, HandleObject proto) +SharedArrayBufferObject::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable, + HandleObject proto) { - SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length); + SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length, maxLength, growable); if (!buffer) return nullptr; @@ -341,7 +479,7 @@ SharedArrayBufferObject::addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSi // just live with the risk. const SharedArrayBufferObject& buf = obj->as(); info->objectsNonHeapElementsShared += - buf.byteLength() / buf.rawBufferObject()->refcount(); + buf.rawBufferObject()->allocatedByteLength() / buf.rawBufferObject()->refcount(); } /* static */ void @@ -409,12 +547,15 @@ static const JSPropertySpec static_properties[] = { }; static const JSFunctionSpec prototype_functions[] = { + JS_FN("grow", SharedArrayBufferObject::fun_grow, 1, 0), JS_SELF_HOSTED_FN("slice", "SharedArrayBufferSlice", 2, 0), JS_FS_END }; static const JSPropertySpec prototype_properties[] = { JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0), + JS_PSG("growable", SharedArrayBufferObject::growableGetter, 0), + JS_PSG("maxByteLength", SharedArrayBufferObject::maxByteLengthGetter, 0), JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY), JS_PS_END }; diff --git a/js/src/vm/SharedArrayObject.h b/js/src/vm/SharedArrayObject.h index d6b18b61a3..f2f0b8c150 100644 --- a/js/src/vm/SharedArrayObject.h +++ b/js/src/vm/SharedArrayObject.h @@ -44,7 +44,9 @@ class SharedArrayRawBuffer { private: mozilla::Atomic refcount_; - uint32_t length; + mozilla::Atomic length; + uint32_t maxLength; + bool growable; bool preparedForAsmJS; // A list of structures representing tasks waiting on some @@ -52,9 +54,12 @@ class SharedArrayRawBuffer FutexWaiter* waiters_; protected: - SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, bool preparedForAsmJS) + SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, uint32_t maxLength, bool growable, + bool preparedForAsmJS) : refcount_(1), length(length), + maxLength(maxLength), + growable(growable), preparedForAsmJS(preparedForAsmJS), waiters_(nullptr) { @@ -62,7 +67,11 @@ class SharedArrayRawBuffer } public: - static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length); + static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length, uint32_t maxLength, + bool growable); + static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length) { + return New(cx, length, length, false); + } // This may be called from multiple threads. The caller must take // care of mutual exclusion. @@ -85,6 +94,20 @@ class SharedArrayRawBuffer return length; } + uint32_t maxByteLength() const { + return growable ? maxLength : byteLength(); + } + + uint32_t allocatedByteLength() const { + return growable ? maxLength : byteLength(); + } + + bool isGrowable() const { + return growable; + } + + [[nodiscard]] bool growTo(uint32_t newLength); + bool isPreparedForAsmJS() const { return preparedForAsmJS; } @@ -117,6 +140,9 @@ class SharedArrayRawBuffer class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared { static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool growableGetterImpl(JSContext* cx, const CallArgs& args); + static bool fun_grow_impl(JSContext* cx, const CallArgs& args); public: // RAWBUF_SLOT holds a pointer (as "private" data) to the @@ -128,13 +154,23 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared static const Class class_; static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + static bool growableGetter(JSContext* cx, unsigned argc, Value* vp); + static bool fun_grow(JSContext* cx, unsigned argc, Value* vp); static bool class_constructor(JSContext* cx, unsigned argc, Value* vp); // Create a SharedArrayBufferObject with a new SharedArrayRawBuffer. static SharedArrayBufferObject* New(JSContext* cx, uint32_t length, + uint32_t maxLength, + bool growable, HandleObject proto = nullptr); + static SharedArrayBufferObject* New(JSContext* cx, + uint32_t length, + HandleObject proto = nullptr) { + return New(cx, length, length, false, proto); + } // Create a SharedArrayBufferObject using an existing SharedArrayRawBuffer. static SharedArrayBufferObject* New(JSContext* cx, @@ -164,6 +200,15 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared uint32_t byteLength() const { return rawBufferObject()->byteLength(); } + uint32_t maxByteLength() const { + return rawBufferObject()->maxByteLength(); + } + bool isGrowable() const { + return rawBufferObject()->isGrowable(); + } + [[nodiscard]] bool growTo(uint32_t newLength) { + return rawBufferObject()->growTo(newLength); + } bool isPreparedForAsmJS() const { return rawBufferObject()->isPreparedForAsmJS(); } From 22cb023133ec05a3023979f69d58149a2aec57c3 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 19:16:34 -0400 Subject: [PATCH 05/19] Implement Atomics.waitAsync --- js/src/builtin/AtomicsObject.cpp | 333 ++++++++++++++++++++--- js/src/builtin/AtomicsObject.h | 1 + js/src/tests/non262/Atomics/waitAsync.js | 37 +++ 3 files changed, 339 insertions(+), 32 deletions(-) create mode 100644 js/src/tests/non262/Atomics/waitAsync.js diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp index a4ae0736d4..72b5ccdcc5 100644 --- a/js/src/builtin/AtomicsObject.cpp +++ b/js/src/builtin/AtomicsObject.cpp @@ -51,6 +51,7 @@ #include "mozilla/Maybe.h" #include "mozilla/Unused.h" +#include "builtin/Promise.h" #include "jsapi.h" #include "jsfriendapi.h" #include "jsnum.h" @@ -59,6 +60,7 @@ #include "jit/InlinableNatives.h" #include "js/Class.h" #include "vm/GlobalObject.h" +#include "vm/HelperThreads.h" #include "vm/Time.h" #include "vm/TypedArrayObject.h" #include "wasm/WasmInstance.h" @@ -693,11 +695,13 @@ js::atomics_cmpxchg_asm_callout(wasm::Instance* instance, int32_t vt, int32_t of namespace js { +class AtomicsWaitAsyncTask; + // Represents one waiting worker. // // The type is declared opaque in SharedArrayObject.h. Instances of -// js::FutexWaiter are stack-allocated and linked onto a list across a -// call to FutexRuntime::wait(). +// js::FutexWaiter are linked onto a list across a call to FutexRuntime::wait() +// or an async wait task. // // The 'waiters' field of the SharedArrayRawBuffer points to the highest // priority waiter in the list, and lower priority nodes are linked through @@ -711,14 +715,34 @@ class FutexWaiter public: FutexWaiter(uint32_t offset, JSRuntime* rt) : offset(offset), + kind(Sync), rt(rt), + asyncTask(nullptr), lower_pri(nullptr), back(nullptr) { } + FutexWaiter(uint32_t offset, AtomicsWaitAsyncTask* asyncTask) + : offset(offset), + kind(Async), + rt(nullptr), + asyncTask(asyncTask), + lower_pri(nullptr), + back(nullptr) + { + } + + bool isWaiting() const; + void notify(); + uint32_t offset; // int32 element index within the SharedArrayBuffer + enum WaiterKind { + Sync, + Async + } kind; JSRuntime* rt; // The runtime of the waiter + AtomicsWaitAsyncTask* asyncTask; // The async waiter task, if any FutexWaiter* lower_pri; // Lower priority nodes in circular doubly-linked list of waiters FutexWaiter* back; // Other direction }; @@ -742,8 +766,195 @@ class AutoLockFutexAPI js::UniqueLock& unique() { return *unique_; } }; +static void +AddWaiter(SharedArrayRawBuffer* sarb, FutexWaiter* waiter) +{ + if (FutexWaiter* waiters = sarb->waiters()) { + waiter->lower_pri = waiters; + waiter->back = waiters->back; + waiters->back->lower_pri = waiter; + waiters->back = waiter; + } else { + waiter->lower_pri = waiter->back = waiter; + sarb->setWaiters(waiter); + } +} + +static void +RemoveWaiter(SharedArrayRawBuffer* sarb, FutexWaiter* waiter) +{ + if (waiter->lower_pri == waiter) { + sarb->setWaiters(nullptr); + } else { + waiter->lower_pri->back = waiter->back; + waiter->back->lower_pri = waiter->lower_pri; + if (sarb->waiters() == waiter) + sarb->setWaiters(waiter->lower_pri); + } + waiter->lower_pri = nullptr; + waiter->back = nullptr; +} + +class AtomicsWaitAsyncTask : public PromiseTask +{ + enum class State { + Waiting, + Notified, + TimedOut + }; + + SharedArrayRawBuffer* sarb_; + FutexWaiter waiter_; + mozilla::Maybe timeout_; + ConditionVariable cond_; + State state_; + bool isInWaiterList_; + + public: + AtomicsWaitAsyncTask(JSContext* cx, Handle promise, + SharedArrayRawBuffer* sarb, uint32_t offset, + mozilla::Maybe& timeout) + : PromiseTask(cx, promise), + sarb_(sarb), + waiter_(offset, this), + timeout_(timeout), + state_(State::Waiting), + isInWaiterList_(false) + { + } + + ~AtomicsWaitAsyncTask() { + if (isInWaiterList_) { + AutoLockFutexAPI lock; + if (isInWaiterList_) + removeFromWaiterList(); + } + sarb_->dropReference(); + } + + FutexWaiter* waiter() { + return &waiter_; + } + + void setInWaiterList() { + isInWaiterList_ = true; + } + + bool isWaiting() const { + return state_ == State::Waiting; + } + + void notify() { + MOZ_ASSERT(isWaiting()); + state_ = State::Notified; + cond_.notify_all(); + } + + void execute() override { + AutoLockFutexAPI lock; + + const bool isTimed = timeout_.isSome(); + auto finalEnd = timeout_.map([](mozilla::TimeDuration& timeout) { + return mozilla::TimeStamp::Now() + timeout; + }); + auto maxSlice = mozilla::TimeDuration::FromSeconds(4000.0); + + while (state_ == State::Waiting) { + if (isTimed) { + auto sliceEnd = finalEnd.map([&](mozilla::TimeStamp& finalEnd) { + auto sliceEnd = mozilla::TimeStamp::Now() + maxSlice; + if (finalEnd < sliceEnd) + sliceEnd = finalEnd; + return sliceEnd; + }); + + mozilla::Unused << cond_.wait_until(lock.unique(), *sliceEnd); + if (state_ == State::Waiting && mozilla::TimeStamp::Now() >= *finalEnd) { + state_ = State::TimedOut; + break; + } + } else { + cond_.wait(lock.unique()); + } + } + + if (isInWaiterList_) + removeFromWaiterList(); + } + + private: + void removeFromWaiterList() { + MOZ_ASSERT(isInWaiterList_); + RemoveWaiter(sarb_, &waiter_); + isInWaiterList_ = false; + } + + bool finishPromise(JSContext* cx, Handle promise) override { + RootedValue result(cx); + if (state_ == State::TimedOut) + result.setString(cx->names().futexTimedOut); + else + result.setString(cx->names().futexOK); + return PromiseObject::resolve(cx, promise, result); + } +}; + +bool +FutexWaiter::isWaiting() const +{ + if (kind == Sync) + return rt->fx.isWaiting(); + return asyncTask->isWaiting(); +} + +void +FutexWaiter::notify() +{ + if (kind == Sync) { + rt->fx.notify(FutexRuntime::NotifyExplicit); + return; + } + asyncTask->notify(); +} + } // namespace js +static bool +GetWaitTimeout(JSContext* cx, HandleValue timeoutv, + mozilla::Maybe* timeout) +{ + timeout->reset(); + if (!timeoutv.isUndefined()) { + double timeout_ms; + if (!ToNumber(cx, timeoutv, &timeout_ms)) + return false; + if (!mozilla::IsNaN(timeout_ms)) { + if (timeout_ms < 0) + *timeout = mozilla::Some(mozilla::TimeDuration::FromSeconds(0.0)); + else if (!mozilla::IsInfinite(timeout_ms)) + *timeout = mozilla::Some(mozilla::TimeDuration::FromMilliseconds(timeout_ms)); + } + } + return true; +} + +static bool +CreateWaitAsyncResult(JSContext* cx, bool isAsync, HandleValue value, MutableHandleValue rval) +{ + RootedPlainObject obj(cx, NewBuiltinClassInstance(cx)); + if (!obj) + return false; + + RootedValue asyncValue(cx, BooleanValue(isAsync)); + if (!NativeDefineDataProperty(cx, obj, cx->names().async, asyncValue, JSPROP_ENUMERATE)) + return false; + if (!NativeDefineDataProperty(cx, obj, cx->names().value, value, JSPROP_ENUMERATE)) + return false; + + rval.setObject(*obj); + return true; +} + bool js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) { @@ -768,17 +979,8 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) if (!ToInt32(cx, valv, &value)) return false; mozilla::Maybe timeout; - if (!timeoutv.isUndefined()) { - double timeout_ms; - if (!ToNumber(cx, timeoutv, &timeout_ms)) - return false; - if (!mozilla::IsNaN(timeout_ms)) { - if (timeout_ms < 0) - timeout = mozilla::Some(mozilla::TimeDuration::FromSeconds(0.0)); - else if (!mozilla::IsInfinite(timeout_ms)) - timeout = mozilla::Some(mozilla::TimeDuration::FromMilliseconds(timeout_ms)); - } - } + if (!GetWaitTimeout(cx, timeoutv, &timeout)) + return false; if (!rt->fx.canWait()) return ReportCannotWait(cx); @@ -797,15 +999,7 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) SharedArrayRawBuffer* sarb = sab->rawBufferObject(); FutexWaiter w(offset, rt); - if (FutexWaiter* waiters = sarb->waiters()) { - w.lower_pri = waiters; - w.back = waiters->back; - waiters->back->lower_pri = &w; - waiters->back = &w; - } else { - w.lower_pri = w.back = &w; - sarb->setWaiters(&w); - } + AddWaiter(sarb, &w); FutexRuntime::WaitResult result = FutexRuntime::FutexOK; bool retval = rt->fx.wait(cx, lock.unique(), timeout, &result); @@ -820,17 +1014,91 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) } } - if (w.lower_pri == &w) { - sarb->setWaiters(nullptr); - } else { - w.lower_pri->back = w.back; - w.back->lower_pri = w.lower_pri; - if (sarb->waiters() == &w) - sarb->setWaiters(w.lower_pri); - } + RemoveWaiter(sarb, &w); return retval; } +bool +js::atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue objv = args.get(0); + HandleValue idxv = args.get(1); + HandleValue valv = args.get(2); + HandleValue timeoutv = args.get(3); + MutableHandleValue r = args.rval(); + + Rooted view(cx, nullptr); + if (!GetSharedTypedArray(cx, objv, &view)) + return false; + if (view->type() != Scalar::Int32) + return ReportBadArrayType(cx); + uint32_t offset; + if (!GetTypedArrayIndex(cx, idxv, view, &offset)) + return false; + int32_t value; + if (!ToInt32(cx, valv, &value)) + return false; + mozilla::Maybe timeout; + if (!GetWaitTimeout(cx, timeoutv, &timeout)) + return false; + + Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); + if (!promise) + return false; + + RootedValue promiseValue(cx, ObjectValue(*promise)); + RootedValue result(cx); + if (!CreateWaitAsyncResult(cx, true, promiseValue, &result)) + return false; + + Rooted sab(cx, view->bufferShared()); + SharedArrayRawBuffer* sarb = sab->rawBufferObject(); + if (!sarb->addReference()) { + JS_ReportErrorASCII(cx, "Reference count overflow on SharedArrayBuffer"); + return false; + } + + auto task = cx->make_unique(cx, promise, sarb, offset, timeout); + if (!task) { + sarb->dropReference(); + return false; + } + + bool isAsync = false; + RootedValue immediateResult(cx); + { + AutoLockFutexAPI lock; + + SharedMem addr = view->viewDataShared().cast() + offset; + if (jit::AtomicOperations::loadSafeWhenRacy(addr) != value) { + immediateResult.setString(cx->names().futexNotEqual); + } else if (timeout.isSome() && timeout->ToMilliseconds() == 0.0) { + immediateResult.setString(cx->names().futexTimedOut); + } else { + if (!cx->startAsyncTaskCallback || !cx->finishAsyncTaskCallback || + !CanUseExtraThreads()) + { + JS_ReportErrorASCII(cx, "Atomics.waitAsync not supported in this runtime."); + return false; + } + + AddWaiter(sarb, task->waiter()); + task->setInWaiterList(); + isAsync = true; + } + } + + if (!isAsync) + return CreateWaitAsyncResult(cx, false, immediateResult, r); + + if (!StartPromiseTask(cx, Move(task))) + return false; + + r.set(result); + return true; +} + bool js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) { @@ -870,9 +1138,9 @@ js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) do { FutexWaiter* c = iter; iter = iter->lower_pri; - if (c->offset != offset || !c->rt->fx.isWaiting()) + if (c->offset != offset || !c->isWaiting()) continue; - c->rt->fx.notify(FutexRuntime::NotifyExplicit); + c->notify(); ++woken; --count; } while (count > 0 && iter != waiters); @@ -1106,6 +1374,7 @@ const JSFunctionSpec AtomicsMethods[] = { JS_INLINABLE_FN("xor", atomics_xor, 3,0, AtomicsXor), JS_INLINABLE_FN("isLockFree", atomics_isLockFree, 1,0, AtomicsIsLockFree), JS_FN("wait", atomics_wait, 4,0), + JS_FN("waitAsync", atomics_waitAsync, 4,0), JS_FN("notify", atomics_notify, 3,0), JS_FN("wake", atomics_notify, 3,0), //Legacy name JS_FS_END diff --git a/js/src/builtin/AtomicsObject.h b/js/src/builtin/AtomicsObject.h index 18f00dad16..a8e2f24e21 100644 --- a/js/src/builtin/AtomicsObject.h +++ b/js/src/builtin/AtomicsObject.h @@ -35,6 +35,7 @@ MOZ_MUST_USE bool atomics_or(JSContext* cx, unsigned argc, Value* vp); MOZ_MUST_USE bool atomics_xor(JSContext* cx, unsigned argc, Value* vp); MOZ_MUST_USE bool atomics_isLockFree(JSContext* cx, unsigned argc, Value* vp); MOZ_MUST_USE bool atomics_wait(JSContext* cx, unsigned argc, Value* vp); +MOZ_MUST_USE bool atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp); MOZ_MUST_USE bool atomics_notify(JSContext* cx, unsigned argc, Value* vp); /* asm.js callouts */ diff --git a/js/src/tests/non262/Atomics/waitAsync.js b/js/src/tests/non262/Atomics/waitAsync.js new file mode 100644 index 0000000000..ae5c876e3d --- /dev/null +++ b/js/src/tests/non262/Atomics/waitAsync.js @@ -0,0 +1,37 @@ +// |reftest| skip-if(!this.SharedArrayBuffer || !this.Atomics || !this.drainJobQueue) + +if (typeof SharedArrayBuffer === "function" && typeof Atomics === "object" && + typeof drainJobQueue === "function") { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + + let result = Atomics.waitAsync(i32, 0, 1, 10); + assertEq(result.async, false); + assertEq(result.value, "not-equal"); + + result = Atomics.waitAsync(i32, 0, 0, 0); + assertEq(result.async, false); + assertEq(result.value, "timed-out"); + + result = Atomics.waitAsync(i32, 0, 0); + assertEq(result.async, true); + let notified; + result.value.then(value => { + notified = value; + }); + assertEq(Atomics.notify(i32, 0, 1), 1); + drainJobQueue(); + assertEq(notified, "ok"); + + result = Atomics.waitAsync(i32, 0, 0, 1); + assertEq(result.async, true); + let timedOut; + result.value.then(value => { + timedOut = value; + }); + drainJobQueue(); + assertEq(timedOut, "timed-out"); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); From 6f3f17ba86c0978e658839b4787622fd9a6eae1c Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 19:40:53 -0400 Subject: [PATCH 06/19] Implement resizable buffer view semantics --- .../non262/ArrayBuffer/resizable-views.js | 70 ++++++++++ js/src/vm/ArrayBufferObject.cpp | 64 ++++++--- js/src/vm/ArrayBufferObject.h | 3 +- js/src/vm/TypedArrayObject.cpp | 47 +++++-- js/src/vm/TypedArrayObject.h | 131 +++++++++++++++--- 5 files changed, 267 insertions(+), 48 deletions(-) create mode 100644 js/src/tests/non262/ArrayBuffer/resizable-views.js diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js new file mode 100644 index 0000000000..32d2e0e342 --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -0,0 +1,70 @@ +// |reftest| skip-if(!this.SharedArrayBuffer) + +var rab = new ArrayBuffer(4, { maxByteLength: 16 }); +var tracking = new Uint8Array(rab); +var fixed = new Uint8Array(rab, 1, 2); +var bytes = new Uint8Array(rab); + +bytes[1] = 11; +bytes[2] = 22; + +assertEq(tracking.length, 4); +assertEq(tracking.byteLength, 4); +assertEq(tracking.byteOffset, 0); +assertEq(fixed.length, 2); +assertEq(fixed.byteLength, 2); +assertEq(fixed.byteOffset, 1); + +rab.resize(2); +assertEq(tracking.length, 2); +assertEq(tracking.byteLength, 2); +assertEq(fixed.length, 0); +assertEq(fixed.byteLength, 0); +assertEq(fixed.byteOffset, 0); +assertEq(fixed[0], undefined); + +rab.resize(8); +assertEq(tracking.length, 8); +assertEq(tracking.byteLength, 8); +assertEq(fixed.length, 2); +assertEq(fixed.byteLength, 2); +assertEq(fixed.byteOffset, 1); +assertEq(fixed[0], 11); +assertEq(fixed[1], 0); +tracking[6] = 66; +assertEq(new Uint8Array(rab)[6], 66); + +var dv = new DataView(rab, 4); +assertEq(dv.byteOffset, 4); +assertEq(dv.byteLength, 4); +dv.setUint8(0, 44); +assertEq(tracking[4], 44); + +rab.resize(3); +assertEq(dv.byteOffset, 0); +assertEq(dv.byteLength, 0); +assertThrowsInstanceOf(() => dv.getUint8(0), RangeError); + +rab.resize(6); +assertEq(dv.byteOffset, 4); +assertEq(dv.byteLength, 2); +assertEq(dv.getUint8(0), 0); + +var fixedDv = new DataView(rab, 4, 2); +rab.resize(5); +assertEq(fixedDv.byteOffset, 0); +assertEq(fixedDv.byteLength, 0); +rab.resize(6); +assertEq(fixedDv.byteOffset, 4); +assertEq(fixedDv.byteLength, 2); + +var gsab = new SharedArrayBuffer(4, { maxByteLength: 16 }); +var sharedTracking = new Uint8Array(gsab); +assertEq(sharedTracking.length, 4); +gsab.grow(8); +assertEq(sharedTracking.length, 8); +sharedTracking[6] = 33; +assertEq(new Uint8Array(gsab)[6], 33); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 21314e7134..17052a808a 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -432,8 +432,12 @@ ArrayBufferViewFits(ArrayBufferViewObject* view, uint32_t newByteLength) { if (view->is()) { DataViewObject& dataView = view->as(); - uint32_t byteOffset = dataView.byteOffset(); - uint32_t byteLength = dataView.byteLength(); + uint32_t byteOffset = dataView.byteOffsetMaybeOutOfBounds(); + if (byteOffset > newByteLength) + return false; + uint32_t byteLength = dataView.isLengthTracking() + ? newByteLength - byteOffset + : dataView.fixedByteLengthMaybeOutOfBounds(); return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; } @@ -441,8 +445,13 @@ ArrayBufferViewFits(ArrayBufferViewObject* view, uint32_t newByteLength) TypedArrayObject& typedArray = view->as(); if (typedArray.isSharedMemory()) return true; - uint32_t byteOffset = typedArray.byteOffset(); - uint32_t byteLength = typedArray.byteLength(); + uint32_t byteOffset = typedArray.byteOffsetMaybeOutOfBounds(); + if (byteOffset > newByteLength) + return false; + uint32_t byteLength = typedArray.isLengthTracking() + ? newByteLength - byteOffset + : typedArray.fixedLengthMaybeOutOfBounds() * + typedArray.bytesPerElement(); return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; } @@ -532,7 +541,8 @@ ArrayBufferObject::setNewData(FreeOp* fop, BufferContents newContents, OwnsState void ArrayBufferObject::changeViewContents(JSContext* cx, ArrayBufferViewObject* view, - uint8_t* oldDataPointer, BufferContents newContents) + uint8_t* oldDataPointer, BufferContents newContents, + uint32_t newByteLength) { MOZ_ASSERT(!view->isSharedMemory()); @@ -543,7 +553,18 @@ ArrayBufferObject::changeViewContents(JSContext* cx, ArrayBufferViewObject* view uint8_t* viewDataPointer = view->dataPointerUnshared(nogc); if (viewDataPointer) { MOZ_ASSERT(newContents); - ptrdiff_t offset = viewDataPointer - oldDataPointer; + uint32_t offset; + if (view->is()) { + offset = view->as().byteOffsetMaybeOutOfBounds(); + } else if (view->is()) { + offset = view->as().byteOffsetMaybeOutOfBounds(); + } else { + ptrdiff_t oldOffset = viewDataPointer - oldDataPointer; + MOZ_ASSERT(oldOffset >= 0); + offset = uint32_t(oldOffset); + } + if (offset > newByteLength) + offset = 0; viewDataPointer = static_cast(newContents.data()) + offset; view->setDataPointerUnshared(viewDataPointer); } @@ -569,10 +590,10 @@ ArrayBufferObject::changeContents(JSContext* cx, BufferContents newContents, auto& innerViews = cx->compartment()->innerViews.get(); if (InnerViewTable::ViewVector* views = innerViews.maybeViewsUnbarriered(this)) { for (size_t i = 0; i < views->length(); i++) - changeViewContents(cx, (*views)[i], oldDataPointer, newContents); + changeViewContents(cx, (*views)[i], oldDataPointer, newContents, byteLength()); } if (firstView()) - changeViewContents(cx, firstView(), oldDataPointer, newContents); + changeViewContents(cx, firstView(), oldDataPointer, newContents, byteLength()); } void @@ -584,26 +605,29 @@ ArrayBufferObject::changeContentsForResize(JSContext* cx, BufferContents newCont uint8_t* oldDataPointer = dataPointer(); setNewData(cx->runtime()->defaultFreeOp(), newContents, ownsState); + setByteLength(newByteLength); auto& innerViews = cx->compartment()->innerViews.get(); if (InnerViewTable::ViewVector* views = innerViews.maybeViewsUnbarriered(this)) { for (size_t i = 0; i < views->length(); i++) { ArrayBufferViewObject* view = (*views)[i]; - if (ArrayBufferViewFits(view, newByteLength)) - changeViewContents(cx, view, oldDataPointer, newContents); + if (view->is() || view->is()) + changeViewContents(cx, view, oldDataPointer, newContents, newByteLength); + else if (ArrayBufferViewFits(view, newByteLength)) + changeViewContents(cx, view, oldDataPointer, newContents, newByteLength); else NoteViewBufferWasDetached(view, newContents, cx); } } if (firstView()) { - if (ArrayBufferViewFits(firstView(), newByteLength)) - changeViewContents(cx, firstView(), oldDataPointer, newContents); + if (firstView()->is() || firstView()->is()) + changeViewContents(cx, firstView(), oldDataPointer, newContents, newByteLength); + else if (ArrayBufferViewFits(firstView(), newByteLength)) + changeViewContents(cx, firstView(), oldDataPointer, newContents, newByteLength); else NoteViewBufferWasDetached(firstView(), newContents, cx); } - - setByteLength(newByteLength); } static bool @@ -1455,20 +1479,22 @@ ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args /* * This method is only called for |DataView(alienBuf, ...)| which calls * this as |createDataViewForThis.call(alienBuf, byteOffset, byteLength, - * DataView.prototype)|, - * ergo there must be exactly 3 arguments. + * DataView.prototype, lengthTracking)|, + * ergo there must be exactly 4 arguments. */ - MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args.length() == 4); uint32_t byteOffset = args[0].toPrivateUint32(); uint32_t byteLength = args[1].toPrivateUint32(); + bool lengthTracking = args[3].toBoolean(); Rooted buffer(cx, &args.thisv().toObject().as()); /* * Pop off the passed-along prototype and delegate to normal DataViewObject * construction. */ - JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, &args[2].toObject()); + JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, + &args[2].toObject(), lengthTracking); if (!obj) return false; args.rval().setObject(*obj); @@ -1848,6 +1874,8 @@ ArrayBufferViewObject::trace(JSTracer* trc, JSObject* objArg) // The data may or may not be inline with the buffer. The buffer // can only move during a compacting GC, in which case its // objectMoved hook has already updated the buffer's data pointer. + if (offset > buf.byteLength()) + offset = 0; obj->initPrivate(buf.dataPointer() + offset); } } diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index d0d83c431d..c9dc80a598 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -323,7 +323,8 @@ class ArrayBufferObject : public ArrayBufferObjectMaybeShared private: void changeViewContents(JSContext* cx, ArrayBufferViewObject* view, - uint8_t* oldDataPointer, BufferContents newContents); + uint8_t* oldDataPointer, BufferContents newContents, + uint32_t newByteLength); void setFirstView(ArrayBufferViewObject* view); uint8_t* inlineDataPointer() const; diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index b04ebcf291..552102dc78 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -482,8 +482,9 @@ class TypedArrayObjectTemplate : public TypedArrayObject } static TypedArrayObject* - makeInstance(JSContext* cx, Handle buffer, uint32_t byteOffset, uint32_t len, - HandleObject proto) + makeInstance(JSContext* cx, Handle buffer, + uint32_t byteOffset, uint32_t len, HandleObject proto, + bool lengthTracking = false) { MOZ_ASSERT_IF(!buffer, byteOffset == 0); @@ -547,7 +548,9 @@ class TypedArrayObjectTemplate : public TypedArrayObject #endif } - obj->setFixedSlot(TypedArrayObject::LENGTH_SLOT, Int32Value(len)); + obj->setFixedSlot(TypedArrayObject::LENGTH_SLOT, + Int32Value(lengthTracking ? TypedArrayObject::LENGTH_TRACKING + : int32_t(len))); obj->setFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT, Int32Value(byteOffset)); #ifdef DEBUG @@ -896,6 +899,7 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; // invalid byteOffset } + bool lengthTracking = false; uint32_t len; if (lengthInt == -1) { len = (buffer->byteLength() - byteOffset) / sizeof(NativeType); @@ -904,6 +908,12 @@ class TypedArrayObjectTemplate : public TypedArrayObject JSMSG_TYPED_ARRAY_CONSTRUCT_BOUNDS); return nullptr; // given byte array doesn't map exactly to sizeof(NativeType) * N } + if ((buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable())) + { + lengthTracking = true; + } } else { len = uint32_t(lengthInt); } @@ -922,7 +932,7 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; // byteOffset + len is too big for the arraybuffer } - return makeInstance(cx, buffer, byteOffset, len, proto); + return makeInstance(cx, buffer, byteOffset, len, proto, lengthTracking); } static bool @@ -1857,7 +1867,8 @@ DataViewNewObjectKind(JSContext* cx, uint32_t byteLength, JSObject* proto) DataViewObject* DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* protoArg) + Handle arrayBuffer, JSObject* protoArg, + bool lengthTracking) { if (arrayBuffer->isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); @@ -1899,7 +1910,9 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, DataViewObject& dvobj = obj->as(); dvobj.setFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT, Int32Value(byteOffset)); - dvobj.setFixedSlot(TypedArrayObject::LENGTH_SLOT, Int32Value(byteLength)); + dvobj.setFixedSlot(TypedArrayObject::LENGTH_SLOT, + Int32Value(lengthTracking ? TypedArrayObject::LENGTH_TRACKING + : int32_t(byteLength))); dvobj.setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectValue(*arrayBuffer)); dvobj.initPrivate(arrayBuffer->dataPointer() + byteOffset); @@ -1919,7 +1932,8 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, bool DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, const CallArgs& args, - uint32_t* byteOffsetPtr, uint32_t* byteLengthPtr) + uint32_t* byteOffsetPtr, uint32_t* byteLengthPtr, + bool* lengthTrackingPtr) { if (!IsArrayBuffer(bufobj)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, @@ -1930,6 +1944,7 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons Rooted buffer(cx, &AsArrayBuffer(bufobj)); uint32_t byteOffset = 0; uint32_t byteLength = buffer->byteLength(); + bool lengthTracking = buffer->isResizable() && !args.hasDefined(2); if (args.length() > 1) { if (!ToUint32(cx, args[1], &byteOffset)) @@ -1955,9 +1970,11 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons if (args.get(2).isUndefined()) { byteLength -= byteOffset; + lengthTracking = buffer->isResizable(); } else { if (!ToUint32(cx, args[2], &byteLength)) return false; + lengthTracking = false; if (byteLength > INT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, "2"); @@ -1981,6 +1998,7 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons *byteOffsetPtr = byteOffset; *byteLengthPtr = byteLength; + *lengthTrackingPtr = lengthTracking; return true; } @@ -1992,7 +2010,8 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con assertSameCompartment(cx, bufobj); uint32_t byteOffset, byteLength; - if (!getAndCheckConstructorArgs(cx, bufobj, args, &byteOffset, &byteLength)) + bool lengthTracking; + if (!getAndCheckConstructorArgs(cx, bufobj, args, &byteOffset, &byteLength, &lengthTracking)) return false; RootedObject proto(cx); @@ -2001,7 +2020,8 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con return false; Rooted buffer(cx, &AsArrayBuffer(bufobj)); - JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto); + JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto, + lengthTracking); if (!obj) return false; args.rval().setObject(*obj); @@ -2041,8 +2061,12 @@ DataViewObject::constructWrapped(JSContext* cx, HandleObject bufobj, const CallA // NB: This entails the IsArrayBuffer check uint32_t byteOffset, byteLength; - if (!getAndCheckConstructorArgs(cx, unwrapped, args, &byteOffset, &byteLength)) + bool lengthTracking; + if (!getAndCheckConstructorArgs(cx, unwrapped, args, &byteOffset, &byteLength, + &lengthTracking)) + { return false; + } // Make sure to get the [[Prototype]] for the created view from this // compartment. @@ -2058,11 +2082,12 @@ DataViewObject::constructWrapped(JSContext* cx, HandleObject bufobj, const CallA return false; } - FixedInvokeArgs<3> args2(cx); + FixedInvokeArgs<4> args2(cx); args2[0].set(PrivateUint32Value(byteOffset)); args2[1].set(PrivateUint32Value(byteLength)); args2[2].setObject(*proto); + args2[3].setBoolean(lengthTracking); RootedValue fval(cx, global->createDataViewForThis()); RootedValue thisv(cx, ObjectValue(*bufobj)); diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h index 8b4b0b5092..9d7f667979 100644 --- a/js/src/vm/TypedArrayObject.h +++ b/js/src/vm/TypedArrayObject.h @@ -52,6 +52,8 @@ class TypedArrayObject : public NativeObject "right buffer slot"); // Slot containing length of the view in number of typed elements. + // Length-tracking views on resizable/growable buffers store + // LENGTH_TRACKING here and compute their visible length from the buffer. static const size_t LENGTH_SLOT = 1; static_assert(LENGTH_SLOT == JS_TYPEDARRAYLAYOUT_LENGTH_SLOT, "self-hosted code with burned-in constants must get the " @@ -65,6 +67,8 @@ class TypedArrayObject : public NativeObject static const size_t RESERVED_SLOTS = 3; + static const int32_t LENGTH_TRACKING = -1; + #ifdef DEBUG static const uint8_t ZeroLengthArrayData = 0x4A; #endif @@ -139,15 +143,13 @@ class TypedArrayObject : public NativeObject return tarr->getFixedSlot(BUFFER_SLOT); } static Value byteOffsetValue(TypedArrayObject* tarr) { - Value v = tarr->getFixedSlot(BYTEOFFSET_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(tarr->byteOffset()); } static Value byteLengthValue(TypedArrayObject* tarr) { - return Int32Value(tarr->getFixedSlot(LENGTH_SLOT).toInt32() * tarr->bytesPerElement()); + return Int32Value(tarr->byteLength()); } static Value lengthValue(TypedArrayObject* tarr) { - return tarr->getFixedSlot(LENGTH_SLOT); + return Int32Value(tarr->length()); } static bool @@ -159,14 +161,70 @@ class TypedArrayObject : public NativeObject JSObject* bufferObject() const { return bufferValue(const_cast(this)).toObjectOrNull(); } + bool isLengthTracking() const { + return getFixedSlot(LENGTH_SLOT).toInt32() == LENGTH_TRACKING; + } + bool hasResizableOrGrowableBuffer() const { + if (!hasBuffer()) + return false; + if (isSharedMemory()) + return bufferShared()->isGrowable(); + return bufferUnshared()->isResizable(); + } + uint32_t byteOffsetMaybeOutOfBounds() const { + Value v = getFixedSlot(BYTEOFFSET_SLOT); + MOZ_ASSERT(v.toInt32() >= 0); + return v.toInt32(); + } + uint32_t fixedLengthMaybeOutOfBounds() const { + int32_t length = getFixedSlot(LENGTH_SLOT).toInt32(); + MOZ_ASSERT(length >= 0); + return length; + } + uint32_t bufferByteLength() const { + MOZ_ASSERT(hasBuffer()); + if (isSharedMemory()) + return bufferShared()->byteLength(); + return bufferUnshared()->byteLength(); + } + bool isOutOfBounds() const { + if (!hasBuffer()) + return false; + if (hasDetachedBuffer()) + return true; + + uint32_t bufferByteLength = this->bufferByteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + if (offset > bufferByteLength) + return true; + + if (isLengthTracking()) + return false; + + uint32_t byteLength = fixedLengthMaybeOutOfBounds() * bytesPerElement(); + return byteLength > bufferByteLength - offset; + } uint32_t byteOffset() const { - return byteOffsetValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + return byteOffsetMaybeOutOfBounds(); } uint32_t byteLength() const { - return byteLengthValue(const_cast(this)).toInt32(); + return length() * bytesPerElement(); } uint32_t length() const { - return lengthValue(const_cast(this)).toInt32(); + if (!isLengthTracking()) { + if (isOutOfBounds()) + return 0; + return fixedLengthMaybeOutOfBounds(); + } + + if (isOutOfBounds()) + return 0; + + uint32_t bufferByteLength = this->bufferByteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + return (bufferByteLength - offset) / bytesPerElement(); } bool hasInlineElements() const; @@ -466,28 +524,26 @@ class DataViewObject : public NativeObject defineGetter(JSContext* cx, PropertyName* name, HandleNativeObject proto); static bool getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, const CallArgs& args, - uint32_t *byteOffset, uint32_t* byteLength); + uint32_t *byteOffset, uint32_t* byteLength, + bool* lengthTracking); static bool constructSameCompartment(JSContext* cx, HandleObject bufobj, const CallArgs& args); static bool constructWrapped(JSContext* cx, HandleObject bufobj, const CallArgs& args); friend bool ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args); static DataViewObject* create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* proto); + Handle arrayBuffer, JSObject* proto, + bool lengthTracking = false); public: static const Class class_; static Value byteOffsetValue(DataViewObject* view) { - Value v = view->getFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(view->byteOffset()); } static Value byteLengthValue(DataViewObject* view) { - Value v = view->getFixedSlot(TypedArrayObject::LENGTH_SLOT); - MOZ_ASSERT(v.toInt32() >= 0); - return v; + return Int32Value(view->byteLength()); } static Value bufferValue(DataViewObject* view) { @@ -495,11 +551,17 @@ class DataViewObject : public NativeObject } uint32_t byteOffset() const { - return byteOffsetValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + return byteOffsetMaybeOutOfBounds(); } uint32_t byteLength() const { - return byteLengthValue(const_cast(this)).toInt32(); + if (isOutOfBounds()) + return 0; + if (isLengthTracking()) + return arrayBuffer().byteLength() - byteOffsetMaybeOutOfBounds(); + return fixedByteLengthMaybeOutOfBounds(); } ArrayBufferObject& arrayBuffer() const { @@ -510,6 +572,39 @@ class DataViewObject : public NativeObject return getPrivate(); } + bool isLengthTracking() const { + return getFixedSlot(TypedArrayObject::LENGTH_SLOT).toInt32() == + TypedArrayObject::LENGTH_TRACKING; + } + + uint32_t byteOffsetMaybeOutOfBounds() const { + Value v = getFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT); + MOZ_ASSERT(v.toInt32() >= 0); + return v.toInt32(); + } + + uint32_t fixedByteLengthMaybeOutOfBounds() const { + int32_t length = getFixedSlot(TypedArrayObject::LENGTH_SLOT).toInt32(); + MOZ_ASSERT(length >= 0); + return length; + } + + bool isOutOfBounds() const { + const ArrayBufferObject& buffer = arrayBuffer(); + if (buffer.isDetached()) + return true; + + uint32_t bufferByteLength = buffer.byteLength(); + uint32_t offset = byteOffsetMaybeOutOfBounds(); + if (offset > bufferByteLength) + return true; + + if (isLengthTracking()) + return false; + + return fixedByteLengthMaybeOutOfBounds() > bufferByteLength - offset; + } + static bool class_constructor(JSContext* cx, unsigned argc, Value* vp); static bool getInt8Impl(JSContext* cx, const CallArgs& args); From d97a2eb04f47b878fbf247f0c65a7833718e89c5 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 20:01:02 -0400 Subject: [PATCH 07/19] Guard typed array JIT paths for resizable buffers --- js/src/jit/BaselineIC.cpp | 13 ++++ js/src/jit/CodeGenerator.cpp | 10 ++- js/src/jit/CodeGenerator.h | 3 +- js/src/jit/IonCaches.cpp | 17 +++++ js/src/jit/MCallOptimize.cpp | 72 ++++--------------- js/src/jit/MIR.cpp | 26 +++---- js/src/jit/SharedIC.cpp | 11 +++ js/src/jit/SharedIC.h | 4 ++ js/src/jsfriendapi.h | 6 +- .../non262/ArrayBuffer/resizable-views.js | 53 ++++++++++++++ js/src/vm/NativeObject.cpp | 22 +++++- js/src/vm/NativeObject.h | 26 ++++++- js/src/vm/TypedArrayObject.cpp | 7 ++ 13 files changed, 185 insertions(+), 85 deletions(-) diff --git a/js/src/jit/BaselineIC.cpp b/js/src/jit/BaselineIC.cpp index a6adc122c7..e61cd065d1 100644 --- a/js/src/jit/BaselineIC.cpp +++ b/js/src/jit/BaselineIC.cpp @@ -1464,6 +1464,12 @@ TryAttachGetElemStub(JSContext* cx, JSScript* script, jsbytecode* pc, ICGetElem_ res.isNumber() && !TypedArrayGetElemStubExists(stub, obj)) { + if (obj->is() && + obj->as().hasResizableOrGrowableBuffer()) + { + return true; + } + if (!cx->runtime()->jitSupportsFloatingPoint && (TypedThingRequiresFloatingPoint(obj) || rhs.isDouble())) { @@ -2154,6 +2160,8 @@ ICGetElem_TypedArray::Compiler::generateStubCode(MacroAssembler& masm) Register obj = masm.extractObject(R0, ExtractTemp0); masm.loadPtr(Address(ICStubReg, ICGetElem_TypedArray::offsetOfShape()), scratchReg); masm.branchTestObjShape(Assembler::NotEqual, obj, scratchReg, &failure); + if (layout_ == Layout_TypedArray) + GuardResizableOrGrowableTypedArray(masm, obj, scratchReg, &failure); // Ensure the index is an integer. if (cx->runtime()->jitSupportsFloatingPoint) { @@ -2625,6 +2633,9 @@ DoSetElemFallback(JSContext* cx, BaselineFrame* frame, ICSetElem_Fallback* stub_ bool expectOutOfBounds; double idx = index.toNumber(); if (obj->is()) { + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + expectOutOfBounds = (idx < 0 || idx >= double(obj->as().length())); } else { // Typed objects throw on out of bounds accesses. Don't attach @@ -3215,6 +3226,8 @@ ICSetElem_TypedArray::Compiler::generateStubCode(MacroAssembler& masm) Register obj = masm.extractObject(R0, ExtractTemp0); masm.loadPtr(Address(ICStubReg, ICSetElem_TypedArray::offsetOfShape()), scratchReg); masm.branchTestObjShape(Assembler::NotEqual, obj, scratchReg, &failure); + if (layout_ == Layout_TypedArray) + GuardResizableOrGrowableTypedArray(masm, obj, scratchReg, &failure); // Ensure the index is an integer. if (cx->runtime()->jitSupportsFloatingPoint) { diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp index 3aa6fba49d..c7d6feed05 100644 --- a/js/src/jit/CodeGenerator.cpp +++ b/js/src/jit/CodeGenerator.cpp @@ -8889,9 +8889,17 @@ CodeGenerator::branchIfNotEmptyObjectElements(Register obj, Label* target) Address(obj, NativeObject::offsetOfElements()), ImmPtr(js::emptyObjectElements), &emptyObj); - masm.branchPtr(Assembler::NotEqual, + masm.branchPtr(Assembler::Equal, Address(obj, NativeObject::offsetOfElements()), ImmPtr(js::emptyObjectElementsShared), + &emptyObj); + masm.branchPtr(Assembler::Equal, + Address(obj, NativeObject::offsetOfElements()), + ImmPtr(js::emptyObjectElementsResizableOrGrowable), + &emptyObj); + masm.branchPtr(Assembler::NotEqual, + Address(obj, NativeObject::offsetOfElements()), + ImmPtr(js::emptyObjectElementsSharedResizableOrGrowable), target); masm.bind(&emptyObj); } diff --git a/js/src/jit/CodeGenerator.h b/js/src/jit/CodeGenerator.h index b0fd03538d..2027ce2df4 100644 --- a/js/src/jit/CodeGenerator.h +++ b/js/src/jit/CodeGenerator.h @@ -535,8 +535,7 @@ class CodeGenerator final : public CodeGeneratorSpecific Label* ifDoesntEmulateUndefined, Register scratch, OutOfLineTestObject* ool); - // Branch to target unless obj has an emptyObjectElements or emptyObjectElementsShared - // elements pointer. + // Branch to target unless obj has one of the empty elements pointers. void branchIfNotEmptyObjectElements(Register obj, Label* target); void emitStoreElementTyped(const LAllocation* value, MIRType valueType, MIRType elementType, diff --git a/js/src/jit/IonCaches.cpp b/js/src/jit/IonCaches.cpp index 5a7e437285..095dc14175 100644 --- a/js/src/jit/IonCaches.cpp +++ b/js/src/jit/IonCaches.cpp @@ -1245,6 +1245,7 @@ GenerateTypedArrayLength(JSContext* cx, MacroAssembler& masm, IonCache::StubAtta masm.branchPtr(Assembler::AboveOrEqual, tmpReg, ImmPtr(&TypedArrayObject::classes[Scalar::MaxTypedArrayViewType]), failures); + GuardResizableOrGrowableTypedArray(masm, object, tmpReg, failures); // Load length. masm.loadTypedOrValue(Address(object, TypedArrayObject::lengthOffset()), output); @@ -1644,6 +1645,9 @@ GetPropertyIC::tryAttachTypedArrayLength(JSContext* cx, HandleScript outerScript if (!JSID_IS_ATOM(id, cx->names().length)) return true; + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + if (hasTypedArrayLengthStub(obj)) return true; @@ -4038,6 +4042,12 @@ GetPropertyIC::canAttachTypedOrUnboxedArrayElement(JSObject* obj, const Value& i if (!obj->is() && !obj->is()) return false; + if (obj->is() && + obj->as().hasResizableOrGrowableBuffer()) + { + return false; + } + MOZ_ASSERT(idval.isInt32() || idval.isString()); // Don't emit a stub if the access is out of bounds. We make to make @@ -4092,6 +4102,9 @@ GenerateGetTypedOrUnboxedArrayElement(JSContext* cx, MacroAssembler& masm, // Decide to what type index the stub should be optimized Register tmpReg = output.scratchReg().gpr(); MOZ_ASSERT(tmpReg != InvalidReg); + if (array->is()) + GuardResizableOrGrowableTypedArray(masm, object, tmpReg, &failures); + Register indexReg = tmpReg; if (idval.isString()) { MOZ_ASSERT(GetIndexFromString(idval.toString()) != UINT32_MAX); @@ -4575,6 +4588,7 @@ GenerateSetTypedArrayElement(JSContext* cx, MacroAssembler& masm, IonCache::Stub if (!shape) return false; masm.branchTestObjShape(Assembler::NotEqual, object, shape, &failures); + GuardResizableOrGrowableTypedArray(masm, object, temp, &failures); // Ensure the index is an int32. Register indexReg; @@ -4661,6 +4675,9 @@ SetPropertyIC::tryAttachTypedArrayElement(JSContext* cx, HandleScript outerScrip if (!IsTypedArrayElementSetInlineable(obj, idval, val)) return true; + if (obj->as().hasResizableOrGrowableBuffer()) + return true; + *emitted = true; MacroAssembler masm(cx, ion, outerScript, profilerLeavePc_); diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index 0f78ef3f2f..4585a838b4 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -365,13 +365,9 @@ IonBuilder::inlineNativeGetter(CallInfo& callInfo, JSFunction* target) // Try to optimize typed array lengths. if (TypedArrayObject::isOriginalLengthGetter(native)) { - Scalar::Type type = thisTypes->getTypedArrayType(constraints()); - if (type == Scalar::MaxTypedArrayViewType) - return InliningStatus_NotInlined; - - MInstruction* length = addTypedArrayLength(thisArg); - current->push(length); - return InliningStatus_Inlined; + // RAB/GSAB views can have dynamic length or temporarily become + // out-of-bounds. Let the property IC/VM path handle the getter. + return InliningStatus_NotInlined; } // Try to optimize RegExp getters. @@ -2480,21 +2476,10 @@ IsTypedArrayObject(CompilerConstraintList* constraints, MDefinition* def) IonBuilder::InliningStatus IonBuilder::inlinePossiblyWrappedTypedArrayLength(CallInfo& callInfo) { - MOZ_ASSERT(!callInfo.constructing()); - MOZ_ASSERT(callInfo.argc() == 1); - if (callInfo.getArg(0)->type() != MIRType::Object) - return InliningStatus_NotInlined; - if (getInlineReturnType() != MIRType::Int32) - return InliningStatus_NotInlined; + (void) callInfo; - if (!IsTypedArrayObject(constraints(), callInfo.getArg(0))) - return InliningStatus_NotInlined; - - MInstruction* length = addTypedArrayLength(callInfo.getArg(0)); - current->push(length); - - callInfo.setImplicitlyUsedUnchecked(); - return InliningStatus_Inlined; + // RAB/GSAB views require dynamic length semantics. + return InliningStatus_NotInlined; } IonBuilder::InliningStatus @@ -3251,45 +3236,14 @@ bool IonBuilder::atomicsMeetsPreconditions(CallInfo& callInfo, Scalar::Type* arrayType, bool* requiresTagCheck, AtomicCheckResult checkResult) { - if (!JitSupportsAtomics()) - return false; + (void) callInfo; + (void) arrayType; + (void) requiresTagCheck; + (void) checkResult; - if (callInfo.getArg(0)->type() != MIRType::Object) - return false; - - if (callInfo.getArg(1)->type() != MIRType::Int32) - return false; - - // Ensure that the first argument is a TypedArray that maps shared - // memory. - // - // Then check both that the element type is something we can - // optimize and that the return type is suitable for that element - // type. - - TemporaryTypeSet* arg0Types = callInfo.getArg(0)->resultTypeSet(); - if (!arg0Types) - return false; - - TemporaryTypeSet::TypedArraySharedness sharedness; - *arrayType = arg0Types->getTypedArrayType(constraints(), &sharedness); - *requiresTagCheck = sharedness != TemporaryTypeSet::KnownShared; - switch (*arrayType) { - case Scalar::Int8: - case Scalar::Uint8: - case Scalar::Int16: - case Scalar::Uint16: - case Scalar::Int32: - return checkResult == DontCheckAtomicResult || getInlineReturnType() == MIRType::Int32; - case Scalar::Uint32: - // Bug 1077305: it would be attractive to allow inlining even - // if the inline return type is Int32, which it will frequently - // be. - return checkResult == DontCheckAtomicResult || getInlineReturnType() == MIRType::Double; - default: - // Excludes floating types and Uint8Clamped. - return false; - } + // Atomics bounds checks through addTypedArrayLengthAndData assume stable + // SharedArrayBuffer lengths. Avoid this path now that growable SAB exists. + return false; } void diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp index e7cb0e55f0..e602212c6f 100644 --- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -5505,25 +5505,15 @@ jit::ElementAccessIsTypedArray(CompilerConstraintList* constraints, MDefinition* obj, MDefinition* id, Scalar::Type* arrayType) { - if (obj->mightBeType(MIRType::String)) - return false; + (void) constraints; + (void) obj; + (void) id; + (void) arrayType; - if (id->type() != MIRType::Int32 && id->type() != MIRType::Double) - return false; - - TemporaryTypeSet* types = obj->resultTypeSet(); - if (!types) - return false; - - *arrayType = types->getTypedArrayType(constraints); - - // FIXME: https://bugzil.la/1536699 - if (*arrayType == Scalar::MaxTypedArrayViewType || - Scalar::isBigIntType(*arrayType)) { - return false; - } - - return true; + // Resizable ArrayBuffer and growable SharedArrayBuffer views need dynamic + // length/out-of-bounds handling. This older MIR path assumes stable + // typed-array length/data slots, so keep element accesses on IC/VM paths. + return false; } bool diff --git a/js/src/jit/SharedIC.cpp b/js/src/jit/SharedIC.cpp index d337534768..c23fe60d74 100644 --- a/js/src/jit/SharedIC.cpp +++ b/js/src/jit/SharedIC.cpp @@ -3608,6 +3608,17 @@ CheckForTypedObjectWithDetachedStorage(JSContext* cx, MacroAssembler& masm, Labe masm.branch32(Assembler::NotEqual, AbsoluteAddress(address), Imm32(0), failure); } +void +GuardResizableOrGrowableTypedArray(MacroAssembler& masm, Register obj, Register scratch, + Label* failure) +{ + masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch); + masm.branchTest32(Assembler::NonZero, + Address(scratch, ObjectElements::offsetOfFlags()), + Imm32(ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER), + failure); +} + void LoadTypedThingData(MacroAssembler& masm, TypedThingLayout layout, Register obj, Register result) { diff --git a/js/src/jit/SharedIC.h b/js/src/jit/SharedIC.h index d259ebf0bc..ba85db660f 100644 --- a/js/src/jit/SharedIC.h +++ b/js/src/jit/SharedIC.h @@ -2302,6 +2302,10 @@ CheckDOMProxyExpandoDoesNotShadow(JSContext* cx, MacroAssembler& masm, Register void CheckForTypedObjectWithDetachedStorage(JSContext* cx, MacroAssembler& masm, Label* failure); +void +GuardResizableOrGrowableTypedArray(MacroAssembler& masm, Register obj, Register scratch, + Label* failure); + MOZ_MUST_USE bool DoCallNativeGetter(JSContext* cx, HandleFunction callee, HandleObject obj, MutableHandleValue result); diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index c463019dac..2697cb5337 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -1748,6 +1748,9 @@ JS_IsFloat64Array(JSObject* obj); extern JS_FRIEND_API(bool) JS_GetTypedArraySharedness(JSObject* obj); +extern JS_FRIEND_API(uint32_t) +JS_GetTypedArrayLength(JSObject* obj); + /* * Test for specific typed array types (ArrayBufferView subtypes) and return * the unwrapped object if so, else nullptr. Never throws. @@ -1814,8 +1817,7 @@ inline void \ Get ## Type ## ArrayLengthAndData(JSObject* obj, uint32_t* length, bool* isSharedMemory, type** data) \ { \ MOZ_ASSERT(GetObjectClass(obj) == detail::Type ## ArrayClassPtr); \ - const JS::Value& lenSlot = GetReservedSlot(obj, detail::TypedArrayLengthSlot); \ - *length = mozilla::AssertedCast(lenSlot.toInt32()); \ + *length = JS_GetTypedArrayLength(obj); \ *isSharedMemory = JS_GetTypedArraySharedness(obj); \ *data = static_cast(GetObjectPrivate(obj)); \ } diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js index 32d2e0e342..94a409f637 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-views.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -66,5 +66,58 @@ assertEq(sharedTracking.length, 8); sharedTracking[6] = 33; assertEq(new Uint8Array(gsab)[6], 33); +function readLength(view) { + return view.length; +} + +function readElement(view, index) { + return view[index]; +} + +function writeElement(view, index, value) { + view[index] = value; +} + +var normal = new Uint8Array(4); +normal[0] = 7; +for (var i = 0; i < 2000; i++) { + assertEq(readLength(normal), 4); + assertEq(readElement(normal, 0), 7); + writeElement(normal, 1, 8); +} + +var icRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var icTracking = new Uint8Array(icRab); +icTracking[0] = 9; +assertEq(readLength(icTracking), 4); +assertEq(readElement(icTracking, 0), 9); +icRab.resize(0); +assertEq(readLength(icTracking), 0); +assertEq(readElement(icTracking, 0), undefined); +writeElement(icTracking, 0, 1); +icRab.resize(4); +assertEq(readLength(icTracking), 4); +assertEq(readElement(icTracking, 0), 0); +writeElement(icTracking, 0, 12); +assertEq(readElement(icTracking, 0), 12); + +var icFixed = new Uint8Array(icRab, 1, 2); +assertEq(readLength(icFixed), 2); +icRab.resize(2); +assertEq(readLength(icFixed), 0); +assertEq(readElement(icFixed, 0), undefined); +writeElement(icFixed, 0, 55); +icRab.resize(4); +assertEq(readLength(icFixed), 2); +assertEq(readElement(icFixed, 0), 0); + +var icGsab = new SharedArrayBuffer(4, { maxByteLength: 8 }); +var icSharedTracking = new Uint8Array(icGsab); +assertEq(readLength(icSharedTracking), 4); +icGsab.grow(8); +assertEq(readLength(icSharedTracking), 8); +writeElement(icSharedTracking, 5, 77); +assertEq(readElement(icSharedTracking, 5), 77); + if (typeof reportCompare === "function") reportCompare(true, true); diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp index cde86fb829..b34a20c91b 100644 --- a/js/src/vm/NativeObject.cpp +++ b/js/src/vm/NativeObject.cpp @@ -43,6 +43,22 @@ static const ObjectElements emptyElementsHeaderShared(0, 0, ObjectElements::Shar HeapSlot* const js::emptyObjectElementsShared = reinterpret_cast(uintptr_t(&emptyElementsHeaderShared) + sizeof(ObjectElements)); +static const ObjectElements emptyElementsHeaderResizableOrGrowable( + 0, 0, ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElementsResizableOrGrowable = + reinterpret_cast(uintptr_t(&emptyElementsHeaderResizableOrGrowable) + + sizeof(ObjectElements)); + +static const ObjectElements emptyElementsHeaderSharedResizableOrGrowable( + 0, 0, ObjectElements::SHARED_MEMORY | ObjectElements::RESIZABLE_OR_GROWABLE_BUFFER); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElementsSharedResizableOrGrowable = + reinterpret_cast(uintptr_t(&emptyElementsHeaderSharedResizableOrGrowable) + + sizeof(ObjectElements)); + #ifdef DEBUG @@ -61,10 +77,12 @@ ObjectElements::ConvertElementsToDoubles(JSContext* cx, uintptr_t elementsPtr) * This function is infallible, but has a fallible interface so that it can * be called directly from Ion code. Only arrays can have their dense * elements converted to doubles, and arrays never have empty elements. - */ + */ HeapSlot* elementsHeapPtr = (HeapSlot*) elementsPtr; MOZ_ASSERT(elementsHeapPtr != emptyObjectElements && - elementsHeapPtr != emptyObjectElementsShared); + elementsHeapPtr != emptyObjectElementsShared && + elementsHeapPtr != emptyObjectElementsResizableOrGrowable && + elementsHeapPtr != emptyObjectElementsSharedResizableOrGrowable); ObjectElements* header = ObjectElements::fromElements(elementsHeapPtr); MOZ_ASSERT(!header->shouldConvertDoubleElements()); diff --git a/js/src/vm/NativeObject.h b/js/src/vm/NativeObject.h index 86977d109f..67fd3a7a46 100644 --- a/js/src/vm/NativeObject.h +++ b/js/src/vm/NativeObject.h @@ -185,6 +185,11 @@ class ObjectElements // These elements are set to integrity level "frozen". FROZEN = 0x10, + + // For TypedArrays only: this TypedArray views a resizable + // ArrayBuffer or growable SharedArrayBuffer. JIT fast paths with + // cached length/data assumptions must fall back for these objects. + RESIZABLE_OR_GROWABLE_BUFFER = 0x20, }; private: @@ -255,6 +260,10 @@ class ObjectElements : flags(SHARED_MEMORY), initializedLength(0), capacity(capacity), length(length) {} + constexpr ObjectElements(uint32_t capacity, uint32_t length, uint32_t flags) + : flags(flags), initializedLength(0), capacity(capacity), length(length) + {} + HeapSlot* elements() { return reinterpret_cast(uintptr_t(this) + sizeof(ObjectElements)); } @@ -269,6 +278,10 @@ class ObjectElements return flags & SHARED_MEMORY; } + bool hasResizableOrGrowableBuffer() const { + return flags & RESIZABLE_OR_GROWABLE_BUFFER; + } + GCPtrNativeObject& ownerObject() const { MOZ_ASSERT(isCopyOnWrite()); return *(GCPtrNativeObject*)(&elements()[initializedLength]); @@ -326,6 +339,8 @@ static_assert(ObjectElements::VALUES_PER_HEADER * sizeof(HeapSlot) == sizeof(Obj */ extern HeapSlot* const emptyObjectElements; extern HeapSlot* const emptyObjectElementsShared; +extern HeapSlot* const emptyObjectElementsResizableOrGrowable; +extern HeapSlot* const emptyObjectElementsSharedResizableOrGrowable; struct Class; class GCMarker; @@ -480,6 +495,12 @@ class NativeObject : public ShapedObject elements_ = emptyObjectElementsShared; } + void setHasResizableOrGrowableBuffer() { + MOZ_ASSERT(elements_ == emptyObjectElements || elements_ == emptyObjectElementsShared); + elements_ = isSharedMemory() ? emptyObjectElementsSharedResizableOrGrowable + : emptyObjectElementsResizableOrGrowable; + } + bool isInWholeCellBuffer() const { const gc::TenuredCell* cell = &asTenured(); gc::ArenaCellSet* cells = cell->arena()->bufferedCells; @@ -1254,7 +1275,10 @@ class NativeObject : public ShapedObject } inline bool hasEmptyElements() const { - return elements_ == emptyObjectElements || elements_ == emptyObjectElementsShared; + return elements_ == emptyObjectElements || + elements_ == emptyObjectElementsShared || + elements_ == emptyObjectElementsResizableOrGrowable || + elements_ == emptyObjectElementsSharedResizableOrGrowable; } /* diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index 552102dc78..7259f242fd 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -509,12 +509,19 @@ class TypedArrayObjectTemplate : public TypedArrayObject return nullptr; bool isSharedMemory = buffer && IsSharedArrayBuffer(buffer.get()); + bool hasResizableOrGrowableBuffer = + buffer && + ((buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable())); obj->setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectOrNullValue(buffer)); // This is invariant. Self-hosting code that sets BUFFER_SLOT // (if it does) must maintain it, should it need to. if (isSharedMemory) obj->setIsSharedMemory(); + if (hasResizableOrGrowableBuffer) + obj->setHasResizableOrGrowableBuffer(); if (buffer) { obj->initViewData(buffer->dataPointerEither() + byteOffset); From 366476589fd60dc560f7e5132afb113158bb20e0 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 20:15:09 -0400 Subject: [PATCH 08/19] Support DataView on shared array buffers --- .../non262/ArrayBuffer/resizable-views.js | 17 ++++++ js/src/vm/ArrayBufferObject.cpp | 15 +++--- js/src/vm/ArrayBufferObject.h | 2 +- js/src/vm/StructuredClone.cpp | 1 + js/src/vm/TypedArrayObject.cpp | 53 ++++++++++++------- js/src/vm/TypedArrayObject.h | 16 ++++-- 6 files changed, 73 insertions(+), 31 deletions(-) diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js index 94a409f637..40cb0260bf 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-views.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -66,6 +66,23 @@ assertEq(sharedTracking.length, 8); sharedTracking[6] = 33; assertEq(new Uint8Array(gsab)[6], 33); +var sharedDv = new DataView(gsab); +assertEq(sharedDv.buffer, gsab); +assertEq(sharedDv.byteOffset, 0); +assertEq(sharedDv.byteLength, 8); +sharedDv.setUint8(7, 99); +assertEq(sharedTracking[7], 99); +gsab.grow(12); +assertEq(sharedDv.byteLength, 12); +assertEq(sharedDv.getUint8(7), 99); + +var fixedSharedDv = new DataView(gsab, 4, 2); +assertEq(fixedSharedDv.byteOffset, 4); +assertEq(fixedSharedDv.byteLength, 2); +gsab.grow(16); +assertEq(fixedSharedDv.byteOffset, 4); +assertEq(fixedSharedDv.byteLength, 2); + function readLength(view) { return view.length; } diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 17052a808a..540b85acf8 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -1474,7 +1474,7 @@ ArrayBufferObject::createEmpty(JSContext* cx) bool ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args) { - MOZ_ASSERT(IsArrayBuffer(args.thisv())); + MOZ_ASSERT(IsAnyArrayBuffer(args.thisv())); /* * This method is only called for |DataView(alienBuf, ...)| which calls @@ -1487,7 +1487,8 @@ ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args uint32_t byteOffset = args[0].toPrivateUint32(); uint32_t byteLength = args[1].toPrivateUint32(); bool lengthTracking = args[3].toBoolean(); - Rooted buffer(cx, &args.thisv().toObject().as()); + Rooted buffer(cx, + &args.thisv().toObject().as()); /* * Pop off the passed-along prototype and delegate to normal DataViewObject @@ -1505,7 +1506,7 @@ bool ArrayBufferObject::createDataViewForThis(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod(cx, args); + return CallNonGenericMethod(cx, args); } /* static */ ArrayBufferObject::BufferContents @@ -1926,6 +1927,8 @@ ArrayBufferViewObject::dataPointerUnshared(const JS::AutoRequireNoGC& nogc) bool ArrayBufferViewObject::isSharedMemory() { + if (is()) + return as().isSharedMemory(); if (is()) return as().isSharedMemory(); return false; @@ -1957,7 +1960,7 @@ ArrayBufferViewObject::bufferObject(JSContext* cx, Handleas().bufferEither(); } MOZ_ASSERT(thisObject->is()); - return &thisObject->as().arrayBuffer(); + return &thisObject->as().arrayBufferEither(); } /* JS Friend API */ @@ -2208,7 +2211,7 @@ JS_GetArrayBufferViewData(JSObject* obj, bool* isSharedMemory, const JS::AutoChe if (!obj) return nullptr; if (obj->is()) { - *isSharedMemory = false; + *isSharedMemory = obj->as().isSharedMemory(); return obj->as().dataPointer(); } TypedArrayObject& ta = obj->as(); @@ -2278,7 +2281,7 @@ js::GetArrayBufferViewLengthAndData(JSObject* obj, uint32_t* length, bool* isSha : obj->as().byteLength(); if (obj->is()) { - *isSharedMemory = false; + *isSharedMemory = obj->as().isSharedMemory(); *data = static_cast(obj->as().dataPointer()); } else { diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index c9dc80a598..f9e3353b7e 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -82,7 +82,7 @@ ArrayBufferObjectMaybeShared& AsAnyArrayBuffer(HandleValue val); class ArrayBufferObjectMaybeShared : public NativeObject { public: - uint32_t byteLength() { + uint32_t byteLength() const { return AnyArrayBufferByteLength(this); } diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp index e1f1b2777e..50e6162923 100644 --- a/js/src/vm/StructuredClone.cpp +++ b/js/src/vm/StructuredClone.cpp @@ -45,6 +45,7 @@ #include "builtin/MapObject.h" #include "js/Date.h" #include "js/GCHashTable.h" +#include "vm/ArrayBufferObject-inl.h" #include "vm/SavedFrame.h" #include "vm/SharedArrayObject.h" #include "vm/TypedArrayObject.h" diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index 7259f242fd..01dd80f4be 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -1874,7 +1874,7 @@ DataViewNewObjectKind(JSContext* cx, uint32_t byteLength, JSObject* proto) DataViewObject* DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* protoArg, + Handle arrayBuffer, JSObject* protoArg, bool lengthTracking) { if (arrayBuffer->isDetached()) { @@ -1885,7 +1885,6 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, MOZ_ASSERT(byteOffset <= INT32_MAX); MOZ_ASSERT(byteLength <= INT32_MAX); MOZ_ASSERT(byteOffset + byteLength < UINT32_MAX); - MOZ_ASSERT(!arrayBuffer || !arrayBuffer->is()); RootedObject proto(cx, protoArg); RootedObject obj(cx); @@ -1921,18 +1920,25 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, Int32Value(lengthTracking ? TypedArrayObject::LENGTH_TRACKING : int32_t(byteLength))); dvobj.setFixedSlot(TypedArrayObject::BUFFER_SLOT, ObjectValue(*arrayBuffer)); - dvobj.initPrivate(arrayBuffer->dataPointer() + byteOffset); + auto dataPointer = arrayBuffer->dataPointerEither(); + dvobj.initPrivate((dataPointer + byteOffset).unwrap(/*safe - stored as private data*/)); // Include a barrier if the data view's data pointer is in the nursery, as // is done for typed arrays. - if (!IsInsideNursery(obj) && cx->runtime()->gc.nursery.isInside(arrayBuffer->dataPointer())) + if (arrayBuffer->is() && + !IsInsideNursery(obj) && + cx->runtime()->gc.nursery.isInside(dataPointer)) + { cx->runtime()->gc.storeBuffer.putWholeCell(obj); + } // Verify that the private slot is at the expected place MOZ_ASSERT(dvobj.numFixedSlots() == TypedArrayObject::DATA_SLOT); - if (!arrayBuffer->addView(cx, &dvobj)) - return nullptr; + if (arrayBuffer->is()) { + if (!arrayBuffer->as().addView(cx, &dvobj)) + return nullptr; + } return &dvobj; } @@ -1942,28 +1948,35 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons uint32_t* byteOffsetPtr, uint32_t* byteLengthPtr, bool* lengthTrackingPtr) { - if (!IsArrayBuffer(bufobj)) { + if (!IsAnyArrayBuffer(bufobj)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, - "DataView", "ArrayBuffer", bufobj->getClass()->name); + "DataView", "ArrayBuffer or SharedArrayBuffer", + bufobj->getClass()->name); return false; } - Rooted buffer(cx, &AsArrayBuffer(bufobj)); + Rooted buffer(cx, &bufobj->as()); uint32_t byteOffset = 0; uint32_t byteLength = buffer->byteLength(); - bool lengthTracking = buffer->isResizable() && !args.hasDefined(2); + bool isResizableOrGrowable = + (buffer->is() && buffer->as().isResizable()) || + (buffer->is() && + buffer->as().isGrowable()); + bool lengthTracking = isResizableOrGrowable && !args.hasDefined(2); if (args.length() > 1) { - if (!ToUint32(cx, args[1], &byteOffset)) + uint64_t offset; + if (!ToIndex(cx, args[1], &offset)) return false; - if (byteOffset > INT32_MAX) { + if (offset > INT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, "1"); return false; } + byteOffset = uint32_t(offset); } - if (buffer->isDetached()) { + if (buffer->is() && buffer->as().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } @@ -1977,16 +1990,18 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons if (args.get(2).isUndefined()) { byteLength -= byteOffset; - lengthTracking = buffer->isResizable(); + lengthTracking = isResizableOrGrowable; } else { - if (!ToUint32(cx, args[2], &byteLength)) + uint64_t viewByteLength; + if (!ToIndex(cx, args[2], &viewByteLength)) return false; lengthTracking = false; - if (byteLength > INT32_MAX) { + if (viewByteLength > INT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, "2"); return false; } + byteLength = uint32_t(viewByteLength); MOZ_ASSERT(byteOffset + byteLength >= byteOffset, "can't overflow: both numbers are less than INT32_MAX"); @@ -2026,7 +2041,7 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - Rooted buffer(cx, &AsArrayBuffer(bufobj)); + Rooted buffer(cx, &bufobj->as()); JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto, lengthTracking); if (!obj) @@ -2226,7 +2241,7 @@ DataViewObject::read(JSContext* cx, Handle obj, bool isLittleEndian = args.length() >= 2 && ToBoolean(args[1]); // Steps 6-7. - if (obj->arrayBuffer().isDetached()) { + if (obj->arrayBufferEither().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } @@ -2325,7 +2340,7 @@ DataViewObject::write(JSContext* cx, Handle obj, bool isLittleEndian = args.length() >= 3 && ToBoolean(args[2]); // Steps 7-8. - if (obj->arrayBuffer().isDetached()) { + if (obj->arrayBufferEither().isDetached()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h index 9d7f667979..8c08bb870e 100644 --- a/js/src/vm/TypedArrayObject.h +++ b/js/src/vm/TypedArrayObject.h @@ -532,7 +532,7 @@ class DataViewObject : public NativeObject friend bool ArrayBufferObject::createDataViewForThisImpl(JSContext* cx, const CallArgs& args); static DataViewObject* create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, - Handle arrayBuffer, JSObject* proto, + Handle arrayBuffer, JSObject* proto, bool lengthTracking = false); public: @@ -560,12 +560,18 @@ class DataViewObject : public NativeObject if (isOutOfBounds()) return 0; if (isLengthTracking()) - return arrayBuffer().byteLength() - byteOffsetMaybeOutOfBounds(); + return arrayBufferEither().byteLength() - byteOffsetMaybeOutOfBounds(); return fixedByteLengthMaybeOutOfBounds(); } - ArrayBufferObject& arrayBuffer() const { - return bufferValue(const_cast(this)).toObject().as(); + ArrayBufferObjectMaybeShared& arrayBufferEither() const { + return bufferValue(const_cast(this)). + toObject().as(); + } + + bool isSharedMemory() const { + return bufferValue(const_cast(this)). + toObject().is(); } void* dataPointer() const { @@ -590,7 +596,7 @@ class DataViewObject : public NativeObject } bool isOutOfBounds() const { - const ArrayBufferObject& buffer = arrayBuffer(); + const ArrayBufferObjectMaybeShared& buffer = arrayBufferEither(); if (buffer.isDetached()) return true; From 8b09714bbc7724e4c081a514d2e95d76ea3de071 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 20:22:42 -0400 Subject: [PATCH 09/19] Support BigInt Atomics waiters --- js/src/builtin/AtomicsObject.cpp | 180 +++++++++++++++++++++-- js/src/tests/non262/Atomics/waitAsync.js | 40 +++++ 2 files changed, 205 insertions(+), 15 deletions(-) diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp index 72b5ccdcc5..f917db9b21 100644 --- a/js/src/builtin/AtomicsObject.cpp +++ b/js/src/builtin/AtomicsObject.cpp @@ -47,6 +47,7 @@ #include "builtin/AtomicsObject.h" #include "mozilla/Atomics.h" +#include "mozilla/Casting.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" #include "mozilla/Unused.h" @@ -59,6 +60,7 @@ #include "jit/AtomicOperations.h" #include "jit/InlinableNatives.h" #include "js/Class.h" +#include "vm/BigIntType.h" #include "vm/GlobalObject.h" #include "vm/HelperThreads.h" #include "vm/Time.h" @@ -123,6 +125,68 @@ GetTypedArrayIndex(JSContext* cx, HandleValue v, Handle view, return true; } +static bool +IsWaitableTypedArray(Scalar::Type viewType) +{ + return viewType == Scalar::Int32 || viewType == Scalar::BigInt64; +} + +static uint32_t +GetWaiterByteOffset(Handle view, uint32_t offset) +{ + return view->byteOffset() + offset * TypedArrayElemSize(view->type()); +} + +static uint64_t +BigIntToRawBits(Scalar::Type viewType, BigInt* value) +{ + if (viewType == Scalar::BigInt64) + return uint64_t(BigInt::toInt64(value)); + MOZ_ASSERT(viewType == Scalar::BigUint64); + return BigInt::toUint64(value); +} + +static bool +BigIntToRawBits(JSContext* cx, Scalar::Type viewType, HandleValue value, uint64_t* bits) +{ + MOZ_ASSERT(Scalar::isBigIntType(viewType)); + + RootedBigInt bigint(cx, ToBigInt(cx, value)); + if (!bigint) + return false; + + *bits = BigIntToRawBits(viewType, bigint); + return true; +} + +static bool +SetBigIntResult(JSContext* cx, Scalar::Type viewType, uint64_t bits, MutableHandleValue result) +{ + MOZ_ASSERT(Scalar::isBigIntType(viewType)); + + BigInt* bigint = viewType == Scalar::BigInt64 + ? BigInt::createFromInt64(cx, mozilla::BitwiseCast(bits)) + : BigInt::createFromUint64(cx, bits); + if (!bigint) + return false; + + result.setBigInt(bigint); + return true; +} + +static bool +WaitValueMatches(Handle view, uint32_t offset, uint64_t expected) +{ + SharedMem viewData = view->viewDataShared(); + if (view->type() == Scalar::BigInt64) { + return jit::AtomicOperations::loadSafeWhenRacy(viewData.cast() + offset) == + expected; + } + + return uint32_t(jit::AtomicOperations::loadSafeWhenRacy(viewData.cast() + offset)) == + uint32_t(expected); +} + static int32_t CompareExchange(Scalar::Type viewType, int32_t oldCandidate, int32_t newCandidate, SharedMem viewData, uint32_t offset, bool* badArrayType = nullptr) @@ -193,6 +257,20 @@ js::atomics_compareExchange(JSContext* cx, unsigned argc, Value* vp) uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + uint64_t oldCandidate; + if (!BigIntToRawBits(cx, view->type(), oldv, &oldCandidate)) + return false; + uint64_t newCandidate; + if (!BigIntToRawBits(cx, view->type(), newv, &newCandidate)) + return false; + + uint64_t result = jit::AtomicOperations::compareExchangeSeqCst( + view->viewDataShared().cast() + offset, oldCandidate, newCandidate); + return SetBigIntResult(cx, view->type(), result, r); + } + int32_t oldCandidate; if (!ToInt32(cx, oldv, &oldCandidate)) return false; @@ -261,6 +339,22 @@ js::atomics_load(JSContext* cx, unsigned argc, Value* vp) r.setNumber(v); return true; } + case Scalar::BigInt64: { + int64_t v = jit::AtomicOperations::loadSeqCst(viewData.cast() + offset); + BigInt* bigint = BigInt::createFromInt64(cx, v); + if (!bigint) + return false; + r.setBigInt(bigint); + return true; + } + case Scalar::BigUint64: { + uint64_t v = jit::AtomicOperations::loadSeqCst(viewData.cast() + offset); + BigInt* bigint = BigInt::createFromUint64(cx, v); + if (!bigint) + return false; + r.setBigInt(bigint); + return true; + } default: return ReportBadArrayType(cx); } @@ -339,6 +433,25 @@ ExchangeOrStore(JSContext* cx, unsigned argc, Value* vp) uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + RootedBigInt bigint(cx, ToBigInt(cx, valv)); + if (!bigint) + return false; + + uint64_t value = BigIntToRawBits(view->type(), bigint); + if (op == DoStore) { + jit::AtomicOperations::storeSeqCst(view->viewDataShared().cast() + offset, + value); + r.setBigInt(bigint); + return true; + } + + uint64_t result = jit::AtomicOperations::exchangeSeqCst( + view->viewDataShared().cast() + offset, value); + return SetBigIntResult(cx, view->type(), result, r); + } + double integerValue; if (!ToInteger(cx, valv, &integerValue)) return false; @@ -382,6 +495,24 @@ AtomicsBinop(JSContext* cx, HandleValue objv, HandleValue idxv, HandleValue valv uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + + if (Scalar::isBigIntType(view->type())) { + uint64_t value; + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; + + SharedMem addr = view->viewDataShared().cast() + offset; + uint64_t old = jit::AtomicOperations::loadSeqCst(addr); + for (;;) { + uint64_t replacement = T::operate64(old, value); + uint64_t observed = jit::AtomicOperations::compareExchangeSeqCst(addr, old, + replacement); + if (observed == old) + return SetBigIntResult(cx, view->type(), old, r); + old = observed; + } + } + int32_t numberValue; if (!ToInt32(cx, valv, &numberValue)) return false; @@ -436,6 +567,7 @@ class PerformAdd public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchAddSeqCst) static int32_t perform(int32_t x, int32_t y) { return x + y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x + y; } }; bool @@ -450,6 +582,7 @@ class PerformSub public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchSubSeqCst) static int32_t perform(int32_t x, int32_t y) { return x - y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x - y; } }; bool @@ -464,6 +597,7 @@ class PerformAnd public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchAndSeqCst) static int32_t perform(int32_t x, int32_t y) { return x & y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x & y; } }; bool @@ -478,6 +612,7 @@ class PerformOr public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchOrSeqCst) static int32_t perform(int32_t x, int32_t y) { return x | y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x | y; } }; bool @@ -492,6 +627,7 @@ class PerformXor public: INTEGRAL_TYPES_FOR_EACH(jit::AtomicOperations::fetchXorSeqCst) static int32_t perform(int32_t x, int32_t y) { return x ^ y; } + static uint64_t operate64(uint64_t x, uint64_t y) { return x ^ y; } }; bool @@ -736,7 +872,7 @@ class FutexWaiter bool isWaiting() const; void notify(); - uint32_t offset; // int32 element index within the SharedArrayBuffer + uint32_t offset; // byte offset within the SharedArrayBuffer enum WaiterKind { Sync, Async @@ -970,14 +1106,21 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) Rooted view(cx, nullptr); if (!GetSharedTypedArray(cx, objv, &view)) return false; - if (view->type() != Scalar::Int32) + if (!IsWaitableTypedArray(view->type())) return ReportBadArrayType(cx); uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; - int32_t value; - if (!ToInt32(cx, valv, &value)) - return false; + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; + } else { + int32_t int32Value; + if (!ToInt32(cx, valv, &int32Value)) + return false; + value = uint32_t(int32Value); + } mozilla::Maybe timeout; if (!GetWaitTimeout(cx, timeoutv, &timeout)) return false; @@ -989,8 +1132,7 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) // and it provides the necessary memory fence. AutoLockFutexAPI lock; - SharedMem addr = view->viewDataShared().cast() + offset; - if (jit::AtomicOperations::loadSafeWhenRacy(addr) != value) { + if (!WaitValueMatches(view, offset, value)) { r.setString(cx->names().futexNotEqual); return true; } @@ -998,7 +1140,7 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) Rooted sab(cx, view->bufferShared()); SharedArrayRawBuffer* sarb = sab->rawBufferObject(); - FutexWaiter w(offset, rt); + FutexWaiter w(GetWaiterByteOffset(view, offset), rt); AddWaiter(sarb, &w); FutexRuntime::WaitResult result = FutexRuntime::FutexOK; @@ -1031,14 +1173,21 @@ js::atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp) Rooted view(cx, nullptr); if (!GetSharedTypedArray(cx, objv, &view)) return false; - if (view->type() != Scalar::Int32) + if (!IsWaitableTypedArray(view->type())) return ReportBadArrayType(cx); uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; - int32_t value; - if (!ToInt32(cx, valv, &value)) - return false; + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; + } else { + int32_t int32Value; + if (!ToInt32(cx, valv, &int32Value)) + return false; + value = uint32_t(int32Value); + } mozilla::Maybe timeout; if (!GetWaitTimeout(cx, timeoutv, &timeout)) return false; @@ -1070,8 +1219,7 @@ js::atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp) { AutoLockFutexAPI lock; - SharedMem addr = view->viewDataShared().cast() + offset; - if (jit::AtomicOperations::loadSafeWhenRacy(addr) != value) { + if (!WaitValueMatches(view, offset, value)) { immediateResult.setString(cx->names().futexNotEqual); } else if (timeout.isSome() && timeout->ToMilliseconds() == 0.0) { immediateResult.setString(cx->names().futexTimedOut); @@ -1083,6 +1231,7 @@ js::atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp) return false; } + task->waiter()->offset = GetWaiterByteOffset(view, offset); AddWaiter(sarb, task->waiter()); task->setInWaiterList(); isAsync = true; @@ -1111,11 +1260,12 @@ js::atomics_notify(JSContext* cx, unsigned argc, Value* vp) Rooted view(cx, nullptr); if (!GetSharedTypedArray(cx, objv, &view)) return false; - if (view->type() != Scalar::Int32) + if (!IsWaitableTypedArray(view->type())) return ReportBadArrayType(cx); uint32_t offset; if (!GetTypedArrayIndex(cx, idxv, view, &offset)) return false; + offset = GetWaiterByteOffset(view, offset); double count; if (countv.isUndefined()) { count = mozilla::PositiveInfinity(); diff --git a/js/src/tests/non262/Atomics/waitAsync.js b/js/src/tests/non262/Atomics/waitAsync.js index ae5c876e3d..a2b23a4f80 100644 --- a/js/src/tests/non262/Atomics/waitAsync.js +++ b/js/src/tests/non262/Atomics/waitAsync.js @@ -31,6 +31,46 @@ if (typeof SharedArrayBuffer === "function" && typeof Atomics === "object" && }); drainJobQueue(); assertEq(timedOut, "timed-out"); + + if (typeof BigInt64Array === "function") { + const bigSab = new SharedArrayBuffer(16); + const i64 = new BigInt64Array(bigSab); + const u64 = new BigUint64Array(bigSab); + + assertEq(Atomics.store(i64, 0, -1n), -1n); + assertEq(Atomics.load(i64, 0), -1n); + assertEq(Atomics.load(u64, 0), 18446744073709551615n); + assertEq(Atomics.exchange(i64, 0, 7n), -1n); + assertEq(Atomics.compareExchange(i64, 0, 7n, 10n), 7n); + assertEq(Atomics.add(i64, 0, 5n), 10n); + assertEq(Atomics.load(i64, 0), 15n); + assertEq(Atomics.sub(i64, 0, 20n), 15n); + assertEq(Atomics.load(i64, 0), -5n); + assertEq(Atomics.and(u64, 0, 7n), 18446744073709551611n); + assertEq(Atomics.or(u64, 0, 8n), 3n); + assertEq(Atomics.xor(u64, 0, 15n), 11n); + + assertThrowsInstanceOf(() => Atomics.waitAsync(u64, 0, 0n), TypeError); + + result = Atomics.waitAsync(i64, 1, 1n, 10); + assertEq(result.async, false); + assertEq(result.value, "not-equal"); + + result = Atomics.waitAsync(i64, 1, 0n, 0); + assertEq(result.async, false); + assertEq(result.value, "timed-out"); + + let offsetView = new BigInt64Array(bigSab, 8); + result = Atomics.waitAsync(offsetView, 0, 0n); + assertEq(result.async, true); + notified = undefined; + result.value.then(value => { + notified = value; + }); + assertEq(Atomics.notify(i64, 1, 1), 1); + drainJobQueue(); + assertEq(notified, "ok"); + } } if (typeof reportCompare === "function") From aea80980ad1ae2a2113672efdbb1110d1a1998cb Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 20:41:27 -0400 Subject: [PATCH 10/19] Fix resizable DataView out-of-bounds semantics --- js/src/js.msg | 1 + .../non262/ArrayBuffer/resizable-views.js | 38 ++++++++++++++++--- js/src/vm/TypedArrayObject.cpp | 29 +++++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/js/src/js.msg b/js/src/js.msg index d9a6c98764..517e34c38a 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -555,6 +555,7 @@ MSG_DEF(JSMSG_TYPED_ARRAY_BAD_ARGS, 0, JSEXN_TYPEERR, "invalid arguments") MSG_DEF(JSMSG_TYPED_ARRAY_NEGATIVE_ARG,1, JSEXN_RANGEERR, "argument {0} must be >= 0") MSG_DEF(JSMSG_TYPED_ARRAY_DETACHED, 0, JSEXN_TYPEERR, "attempting to access detached ArrayBuffer") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_BOUNDS, 0, JSEXN_RANGEERR, "attempting to construct out-of-bounds TypedArray on ArrayBuffer") +MSG_DEF(JSMSG_DATA_VIEW_OUT_OF_BOUNDS, 0, JSEXN_TYPEERR, "attempting to access out-of-bounds DataView") MSG_DEF(JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, 1, JSEXN_TYPEERR, "cannot directly {0} builtin %TypedArray%") MSG_DEF(JSMSG_NON_TYPED_ARRAY_RETURNED, 0, JSEXN_TYPEERR, "constructor didn't return TypedArray object") MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray of at least length {0}, but constructor returned TypedArray of length {1}") diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js index 40cb0260bf..590b9ce691 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-views.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -41,9 +41,9 @@ dv.setUint8(0, 44); assertEq(tracking[4], 44); rab.resize(3); -assertEq(dv.byteOffset, 0); -assertEq(dv.byteLength, 0); -assertThrowsInstanceOf(() => dv.getUint8(0), RangeError); +assertThrowsInstanceOf(() => dv.byteOffset, TypeError); +assertThrowsInstanceOf(() => dv.byteLength, TypeError); +assertThrowsInstanceOf(() => dv.getUint8(0), TypeError); rab.resize(6); assertEq(dv.byteOffset, 4); @@ -52,12 +52,40 @@ assertEq(dv.getUint8(0), 0); var fixedDv = new DataView(rab, 4, 2); rab.resize(5); -assertEq(fixedDv.byteOffset, 0); -assertEq(fixedDv.byteLength, 0); +assertThrowsInstanceOf(() => fixedDv.byteOffset, TypeError); +assertThrowsInstanceOf(() => fixedDv.byteLength, TypeError); +assertThrowsInstanceOf(() => fixedDv.getUint8(0), TypeError); rab.resize(6); assertEq(fixedDv.byteOffset, 4); assertEq(fixedDv.byteLength, 2); +var ctorRab = new ArrayBuffer(8, { maxByteLength: 8 }); +var ShrinkingNewTarget = new Proxy(function() {}, { + get(target, prop, receiver) { + if (prop === "prototype") { + ctorRab.resize(2); + return DataView.prototype; + } + return Reflect.get(target, prop, receiver); + } +}); +assertThrowsInstanceOf(() => Reflect.construct(DataView, [ctorRab, 4], ShrinkingNewTarget), + RangeError); + +var fixedCtorRab = new ArrayBuffer(8, { maxByteLength: 8 }); +var FixedShrinkingNewTarget = new Proxy(function() {}, { + get(target, prop, receiver) { + if (prop === "prototype") { + fixedCtorRab.resize(5); + return DataView.prototype; + } + return Reflect.get(target, prop, receiver); + } +}); +assertThrowsInstanceOf(() => Reflect.construct(DataView, [fixedCtorRab, 4, 2], + FixedShrinkingNewTarget), + RangeError); + var gsab = new SharedArrayBuffer(4, { maxByteLength: 16 }); var sharedTracking = new Uint8Array(gsab); assertEq(sharedTracking.length, 4); diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index 01dd80f4be..c75d956303 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -1884,7 +1884,18 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, MOZ_ASSERT(byteOffset <= INT32_MAX); MOZ_ASSERT(byteLength <= INT32_MAX); - MOZ_ASSERT(byteOffset + byteLength < UINT32_MAX); + + uint32_t bufferByteLength = arrayBuffer->byteLength(); + if (byteOffset > bufferByteLength || + (!lengthTracking && byteLength > bufferByteLength - byteOffset)) + { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, + "1"); + return nullptr; + } + + if (lengthTracking) + byteLength = bufferByteLength - byteOffset; RootedObject proto(cx, protoArg); RootedObject obj(cx); @@ -1908,9 +1919,6 @@ DataViewObject::create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength, } } - // Caller should have established these preconditions, and no - // (non-self-hosted) JS code has had an opportunity to run so nothing can - // have invalidated them. MOZ_ASSERT(byteOffset <= arrayBuffer->byteLength()); MOZ_ASSERT(byteOffset + byteLength <= arrayBuffer->byteLength()); @@ -2137,6 +2145,11 @@ template /* static */ uint8_t* DataViewObject::getDataPointer(JSContext* cx, Handle obj, uint64_t offset) { + if (obj->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATA_VIEW_OUT_OF_BOUNDS); + return nullptr; + } + const size_t TypeSize = sizeof(NativeType); if (offset > UINT32_MAX - TypeSize || offset + TypeSize > obj->byteLength()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARG_INDEX_OUT_OF_RANGE, @@ -3165,7 +3178,13 @@ template bool DataViewObject::getterImpl(JSContext* cx, const CallArgs& args) { - args.rval().set(ValueGetter(&args.thisv().toObject().as())); + Rooted view(cx, &args.thisv().toObject().as()); + if (ValueGetter != bufferValue && view->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATA_VIEW_OUT_OF_BOUNDS); + return false; + } + + args.rval().set(ValueGetter(view)); return true; } From 3cb76bb20e4d4caed347bd9a0496fd3effbe996b Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 20:55:57 -0400 Subject: [PATCH 11/19] Validate typed array methods on resizable buffers --- js/src/builtin/TypedArray.js | 15 +++++++ js/src/js.msg | 1 + .../non262/ArrayBuffer/resizable-views.js | 41 +++++++++++++++++++ js/src/vm/SelfHosting.cpp | 34 +++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 16603d9b86..1c054ed759 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -41,10 +41,21 @@ function TypedArrayContentTypeIsBigIntMethod() { return IsBigInt64TypedArray(this) || IsBigUint64TypedArray(this); } +function ThrowIfTypedArrayOutOfBounds(tarray) { + if (TypedArrayIsOutOfBounds(tarray)) + ThrowTypeError(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); +} + +function ThrowIfPossiblyWrappedTypedArrayOutOfBounds(tarray) { + if (PossiblyWrappedTypedArrayIsOutOfBounds(tarray)) + ThrowTypeError(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); +} + function GetAttachedArrayBuffer(tarray) { var buffer = ViewedArrayBufferIfReified(tarray); if (IsDetachedBuffer(buffer)) ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfTypedArrayOutOfBounds(tarray); return buffer; } @@ -67,6 +78,7 @@ function IsTypedArrayEnsuringArrayBuffer(arg) { if (IsObject(arg) && IsPossiblyWrappedTypedArray(arg)) { if (PossiblyWrappedTypedArrayHasDetachedBuffer(arg)) ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(arg); return false; } @@ -89,6 +101,7 @@ function ValidateTypedArray(obj, error) { if (IsPossiblyWrappedTypedArray(obj)) { if (PossiblyWrappedTypedArrayHasDetachedBuffer(obj)) ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(obj); return false; } } @@ -1472,6 +1485,8 @@ function TypedArraySubarray(begin, end) { "TypedArraySubarray"); } + GetAttachedArrayBuffer(obj); + // Steps 4-6. var buffer = TypedArrayBuffer(obj); var srcLength = TypedArrayLength(obj); diff --git a/js/src/js.msg b/js/src/js.msg index 517e34c38a..d8547ae4eb 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -555,6 +555,7 @@ MSG_DEF(JSMSG_TYPED_ARRAY_BAD_ARGS, 0, JSEXN_TYPEERR, "invalid arguments") MSG_DEF(JSMSG_TYPED_ARRAY_NEGATIVE_ARG,1, JSEXN_RANGEERR, "argument {0} must be >= 0") MSG_DEF(JSMSG_TYPED_ARRAY_DETACHED, 0, JSEXN_TYPEERR, "attempting to access detached ArrayBuffer") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_BOUNDS, 0, JSEXN_RANGEERR, "attempting to construct out-of-bounds TypedArray on ArrayBuffer") +MSG_DEF(JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS, 0, JSEXN_TYPEERR, "attempting to access out-of-bounds TypedArray") MSG_DEF(JSMSG_DATA_VIEW_OUT_OF_BOUNDS, 0, JSEXN_TYPEERR, "attempting to access out-of-bounds DataView") MSG_DEF(JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, 1, JSEXN_TYPEERR, "cannot directly {0} builtin %TypedArray%") MSG_DEF(JSMSG_NON_TYPED_ARRAY_RETURNED, 0, JSEXN_TYPEERR, "constructor didn't return TypedArray object") diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js index 590b9ce691..81e6328b67 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-views.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -59,6 +59,47 @@ rab.resize(6); assertEq(fixedDv.byteOffset, 4); assertEq(fixedDv.byteLength, 2); +var methodRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var methodFixed = new Uint8Array(methodRab, 2, 2); +methodRab.resize(2); +[ + () => methodFixed.at(0), + () => methodFixed.copyWithin(0, 0), + () => methodFixed.entries(), + () => methodFixed.every(x => true), + () => methodFixed.fill(1), + () => methodFixed.filter(x => true), + () => methodFixed.find(x => true), + () => methodFixed.findIndex(x => true), + () => methodFixed.findLast(x => true), + () => methodFixed.findLastIndex(x => true), + () => methodFixed.forEach(x => x), + () => methodFixed.includes(0), + () => methodFixed.indexOf(0), + () => methodFixed.join(","), + () => methodFixed.keys(), + () => methodFixed.lastIndexOf(0), + () => methodFixed.map(x => x), + () => methodFixed.reduce((a, b) => a + b, 0), + () => methodFixed.reduceRight((a, b) => a + b, 0), + () => methodFixed.reverse(), + () => methodFixed.set([1], 0), + () => methodFixed.slice(), + () => methodFixed.some(x => true), + () => methodFixed.sort(), + () => methodFixed.subarray(), + () => methodFixed.toLocaleString(), + () => methodFixed.toReversed(), + () => methodFixed.toSorted(), + () => methodFixed.toString(), + () => methodFixed.values(), + () => methodFixed.with(0, 1), + () => methodFixed[Symbol.iterator](), +].forEach(fn => assertThrowsInstanceOf(fn, TypeError)); + +methodRab.resize(4); +assertEq(methodFixed.length, 2); + var ctorRab = new ArrayBuffer(8, { maxByteLength: 8 }); var ShrinkingNewTarget = new Proxy(function() {}, { get(target, prop, receiver) { diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 804e231a3f..926739e500 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -1310,6 +1310,36 @@ intrinsic_PossiblyWrappedTypedArrayHasDetachedBuffer(JSContext* cx, unsigned arg return true; } +static bool +intrinsic_TypedArrayIsOutOfBounds(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + RootedObject obj(cx, &args[0].toObject()); + MOZ_ASSERT(obj->is()); + args.rval().setBoolean(obj->as().isOutOfBounds()); + return true; +} + +static bool +intrinsic_PossiblyWrappedTypedArrayIsOutOfBounds(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isObject()); + + JSObject* obj = CheckedUnwrap(&args[0].toObject()); + if (!obj) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return false; + } + + MOZ_ASSERT(obj->is()); + args.rval().setBoolean(obj->as().isOutOfBounds()); + return true; +} + static bool intrinsic_MoveTypedArrayElements(JSContext* cx, unsigned argc, Value* vp) { @@ -2489,6 +2519,10 @@ static const JSFunctionSpec intrinsic_functions[] = { 1, 0, IntrinsicPossiblyWrappedTypedArrayLength), JS_FN("PossiblyWrappedTypedArrayHasDetachedBuffer", intrinsic_PossiblyWrappedTypedArrayHasDetachedBuffer, 1, 0), + JS_FN("TypedArrayIsOutOfBounds", + intrinsic_TypedArrayIsOutOfBounds, 1, 0), + JS_FN("PossiblyWrappedTypedArrayIsOutOfBounds", + intrinsic_PossiblyWrappedTypedArrayIsOutOfBounds, 1, 0), JS_FN("MoveTypedArrayElements", intrinsic_MoveTypedArrayElements, 4,0), JS_FN("SetFromTypedArrayApproach",intrinsic_SetFromTypedArrayApproach, 4, 0), From 7613d2901f57a680d291298af44a7a89202d0c31 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 21:15:01 -0400 Subject: [PATCH 12/19] Validate typed array set and constructors on resizable buffers --- js/src/builtin/TypedArray.js | 8 ++- .../non262/ArrayBuffer/resizable-views.js | 9 +++ js/src/vm/SelfHosting.cpp | 4 ++ js/src/vm/TypedArrayCommon.h | 63 ++++++++++++++++--- js/src/vm/TypedArrayObject.cpp | 4 ++ 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 1c054ed759..31702633e0 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -1143,12 +1143,18 @@ function TypedArraySet(overloaded, offset = 0) { // Steps 9-10. var targetBuffer = GetAttachedArrayBuffer(target); + ThrowIfTypedArrayOutOfBounds(target); + // Step 11. var targetLength = TypedArrayLength(target); // Steps 12 et seq. - if (IsPossiblyWrappedTypedArray(overloaded)) + if (IsPossiblyWrappedTypedArray(overloaded)) { + if (PossiblyWrappedTypedArrayHasDetachedBuffer(overloaded)) + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + ThrowIfPossiblyWrappedTypedArrayOutOfBounds(overloaded); return SetFromTypedArray(target, overloaded, targetOffset, targetLength); + } return SetFromNonTypedArray(target, overloaded, targetOffset, targetLength, targetBuffer); } diff --git a/js/src/tests/non262/ArrayBuffer/resizable-views.js b/js/src/tests/non262/ArrayBuffer/resizable-views.js index 81e6328b67..e8d810b165 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-views.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -100,6 +100,15 @@ methodRab.resize(2); methodRab.resize(4); assertEq(methodFixed.length, 2); +var sourceRab = new ArrayBuffer(4, { maxByteLength: 8 }); +var oobSource = new Uint8Array(sourceRab, 2, 2); +sourceRab.resize(2); +assertThrowsInstanceOf(() => new Uint8Array(oobSource), TypeError); +assertThrowsInstanceOf(() => new Uint16Array(oobSource), TypeError); +assertThrowsInstanceOf(() => new Uint8Array(4).set(oobSource), TypeError); +sourceRab.resize(4); +assertEq(new Uint8Array(oobSource).length, 2); + var ctorRab = new ArrayBuffer(8, { maxByteLength: 8 }); var ShrinkingNewTarget = new Proxy(function() {}, { get(target, prop, receiver) { diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 926739e500..7b61a7ddd1 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -1462,6 +1462,10 @@ intrinsic_SetFromTypedArrayApproach(JSContext* cx, unsigned argc, Value* vp) JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return false; } + if (unsafeTypedArrayCrossCompartment->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } // Steps 21, 23. uint32_t unsafeSrcLengthCrossCompartment = unsafeTypedArrayCrossCompartment->length(); diff --git a/js/src/vm/TypedArrayCommon.h b/js/src/vm/TypedArrayCommon.h index 59ffd78b24..59126158eb 100644 --- a/js/src/vm/TypedArrayCommon.h +++ b/js/src/vm/TypedArrayCommon.h @@ -761,16 +761,40 @@ class TypedArrayMethods if (!ToInt32(cx, args[1], &offset)) return false; - if (offset < 0 || uint32_t(offset) > target->length()) { - // the given offset is bogus + if (offset < 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } } + if (target->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (target->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + uint32_t targetLength = target->length(); + if (uint32_t(offset) > targetLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); + return false; + } + RootedObject arg0(cx, &args[0].toObject()); if (arg0->is()) { - if (arg0->as().length() > target->length() - offset) { + Rooted source(cx, &arg0->as()); + if (source->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (source->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + if (source->length() > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } @@ -782,7 +806,7 @@ class TypedArrayMethods if (!GetLengthProperty(cx, arg0, &len)) return false; - if (uint32_t(offset) > target->length() || len > target->length() - offset) { + if (len > targetLength - offset) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } @@ -795,13 +819,32 @@ class TypedArrayMethods return true; } - static bool - setFromTypedArray(JSContext* cx, Handle target, HandleObject source, - uint32_t offset = 0) - { - MOZ_ASSERT(source->is(), "use setFromNonTypedArray"); + static bool + setFromTypedArray(JSContext* cx, Handle target, HandleObject source, + uint32_t offset = 0) + { + MOZ_ASSERT(source->is(), "use setFromNonTypedArray"); - bool isShared = target->isSharedMemory() || source->as().isSharedMemory(); + if (target->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (target->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + Rooted sourceArray(cx, &source->as()); + if (sourceArray->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + if (sourceArray->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return false; + } + + bool isShared = target->isSharedMemory() || sourceArray->isSharedMemory(); switch (target->type()) { case Scalar::Int8: diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index c75d956303..d3158d78aa 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -1335,6 +1335,10 @@ TypedArrayObjectTemplate::fromTypedArray(JSContext* cx, HandleObject other, b JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED); return nullptr; } + if (srcArray->isOutOfBounds()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_OUT_OF_BOUNDS); + return nullptr; + } // Step 9. uint32_t elementLength = srcArray->length(); From 61912d231848c6e207171f05426a98c10fc513fc Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 21:26:56 -0400 Subject: [PATCH 13/19] Fix ArrayBuffer slice after resizable source shrink --- js/src/builtin/TypedArray.js | 5 +++- .../non262/ArrayBuffer/resizable-transfer.js | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 31702633e0..93214355f2 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -1973,7 +1973,10 @@ function ArrayBufferSlice(start, end) { ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); // Steps 19-21. - ArrayBufferCopyData(new_, 0, O, first | 0, newLen | 0, isWrapped); + var currentLen = ArrayBufferByteLength(O); + var copyLen = first >= currentLen ? 0 : std_Math_min(newLen, currentLen - first); + if (copyLen > 0) + ArrayBufferCopyData(new_, 0, O, first | 0, copyLen | 0, isWrapped); // Step 22. return new_; diff --git a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js index b9f9dbb13a..b05a0deffa 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js @@ -62,6 +62,33 @@ assertEq(fixedMoved.maxByteLength, 10); assertEq(fixedMoved.resizable, false); assertEq(new Uint8Array(fixedMoved)[0], 7); +var sliceSource = new ArrayBuffer(8, { maxByteLength: 8 }); +var sliceSourceBytes = new Uint8Array(sliceSource); +for (var i = 0; i < sliceSourceBytes.length; i++) + sliceSourceBytes[i] = i + 1; + +sliceSource.constructor = { + [Symbol.species]: function(byteLength) { + sliceSource.resize(4); + return new ArrayBuffer(byteLength); + } +}; + +var sliced = sliceSource.slice(2, 8); +var slicedBytes = new Uint8Array(sliced); +assertEq(sliced.byteLength, 6); +assertEq(slicedBytes[0], 3); +assertEq(slicedBytes[1], 4); +assertEq(slicedBytes[2], 0); +assertEq(slicedBytes[5], 0); + +sliceSource.resize(8); +for (var i = 0; i < sliceSourceBytes.length; i++) + sliceSourceBytes[i] = i + 1; +var zeroCopied = sliceSource.slice(6, 8); +assertEq(zeroCopied.byteLength, 2); +assertEq(new Uint8Array(zeroCopied)[0], 0); + assertThrowsInstanceOf(() => new ArrayBuffer(4, { maxByteLength: 3 }), RangeError); if (typeof reportCompare === "function") From e21c4e2917bea0c9fa115e1d135307451deba82f Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 21:42:53 -0400 Subject: [PATCH 14/19] Fix ArrayBuffer storage and error types --- js/src/js.msg | 2 ++ .../non262/ArrayBuffer/resizable-transfer.js | 3 +++ js/src/vm/ArrayBufferObject.cpp | 24 +++++-------------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/js/src/js.msg b/js/src/js.msg index d8547ae4eb..c635bb54ed 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -548,6 +548,8 @@ MSG_DEF(JSMSG_TOO_LONG_ARRAY, 0, JSEXN_TYPEERR, "Too long array") // Typed array MSG_DEF(JSMSG_BAD_INDEX, 0, JSEXN_RANGEERR, "invalid or out-of-range index") +MSG_DEF(JSMSG_ARRAYBUFFER_NOT_RESIZABLE, 0, JSEXN_TYPEERR, "ArrayBuffer is not resizable") +MSG_DEF(JSMSG_ARRAYBUFFER_CANNOT_DETACH, 0, JSEXN_TYPEERR, "ArrayBuffer cannot be detached") MSG_DEF(JSMSG_NON_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected ArrayBuffer, but species constructor returned non-ArrayBuffer") MSG_DEF(JSMSG_SAME_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different ArrayBuffer, but species constructor returned same ArrayBuffer") MSG_DEF(JSMSG_SHORT_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected ArrayBuffer with at least {0} bytes, but species constructor returns ArrayBuffer with {1} bytes") diff --git a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js index b05a0deffa..f603edabff 100644 --- a/js/src/tests/non262/ArrayBuffer/resizable-transfer.js +++ b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js @@ -5,6 +5,9 @@ assertEq(fixed.byteLength, 4); assertEq(fixed.maxByteLength, 4); assertEq(fixed.resizable, false); assertEq(fixed.detached, false); +new Uint8Array(fixed).set([1, 2, 3, 4]); +fixed.extra = 17; +assertEq(Array.from(new Uint8Array(fixed)).join(","), "1,2,3,4"); assertThrowsInstanceOf(() => fixed.resize(2), TypeError); assertEq(ArrayBuffer.prototype.transfer.length, 0); assertEq(ArrayBuffer.prototype.transferToFixedLength.length, 0); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 540b85acf8..3ec0c1f2e9 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -409,14 +409,14 @@ AllocateArrayBufferContents(JSContext* cx, uint32_t nbytes) static bool ReportArrayBufferNotResizable(JSContext* cx) { - JS_ReportErrorASCII(cx, "ArrayBuffer is not resizable"); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_NOT_RESIZABLE); return false; } static bool ReportArrayBufferCannotDetach(JSContext* cx) { - JS_ReportErrorASCII(cx, "ArrayBuffer cannot be detached"); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_CANNOT_DETACH); return false; } @@ -1389,10 +1389,6 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content return nullptr; } - // If we need to allocate data, try to use a larger object size class so - // that the array buffer's data can be allocated inline with the object. - // The extra space will be left unused by the object's fixed slots and - // available for the buffer's data, see NewObject(). size_t reservedSlots = JSCLASS_RESERVED_SLOTS(&class_); size_t nslots = reservedSlots; @@ -1409,18 +1405,10 @@ ArrayBufferObject::create(JSContext* cx, uint32_t nbytes, BufferContents content } } else { MOZ_ASSERT(ownsState == OwnsData); - size_t usableSlots = NativeObject::MAX_FIXED_SLOTS - reservedSlots; - if (nbytes <= usableSlots * sizeof(Value)) { - int newSlots = nbytes == 0 ? 0 : (nbytes - 1) / sizeof(Value) + 1; - MOZ_ASSERT(int(nbytes) <= newSlots * int(sizeof(Value))); - nslots = reservedSlots + newSlots; - contents = BufferContents::createPlain(nullptr); - } else { - contents = AllocateArrayBufferContents(cx, nbytes); - if (!contents) - return nullptr; - allocated = true; - } + contents = AllocateArrayBufferContents(cx, nbytes); + if (!contents) + return nullptr; + allocated = true; } MOZ_ASSERT(!(class_.flags & JSCLASS_HAS_PRIVATE)); From a02580dae4212236f1f0ce249ebc68c1a2f352e1 Mon Sep 17 00:00:00 2001 From: Moonchild Date: Mon, 18 May 2026 12:41:20 +0200 Subject: [PATCH 15/19] Fix incorrect variadic for `size_t` in `fprintf` statement in `hyphen.c`. --- intl/hyphenation/hyphen/hyphen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intl/hyphenation/hyphen/hyphen.c b/intl/hyphenation/hyphen/hyphen.c index 9f2b7112c8..bd7e9a790c 100644 --- a/intl/hyphenation/hyphen/hyphen.c +++ b/intl/hyphenation/hyphen/hyphen.c @@ -446,7 +446,7 @@ for (k = 0; k < 2; k++) { while ((c = fgetc(f)) != '\n' && c != EOF); /* issue warning if not a comment */ if (buf[0] != '%') { - fprintf(stderr, "Warning: skipping too long pattern (more than %lu chars)\n", sizeof(buf)); + fprintf(stderr, "Warning: skipping too long pattern (more than %zu chars)\n", sizeof(buf)); } continue; } From 6861bedff630ca4e26e6bbb46d8690af445412c5 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 11:47:18 -0400 Subject: [PATCH 16/19] Make WeakRef support always enabled --- dom/workers/RuntimeService.cpp | 1 - dom/workers/WorkerPrefs.h | 1 - js/src/builtin/WeakRefObject.cpp | 95 ++++++++++++++++++-------- js/src/builtin/WeakRefObject.h | 25 +++++-- js/src/js.msg | 1 + js/src/jsapi-tests/testGCWeakRef.cpp | 60 +++++++++++++++- js/src/jsapi.cpp | 9 +++ js/src/jsapi.h | 15 ++-- js/src/shell/js.cpp | 5 +- js/src/tests/manual/weakref-smoke.js | 12 ++-- js/src/vm/Runtime.cpp | 58 ++++++++++++++++ js/src/vm/Runtime.h | 7 ++ js/xpconnect/src/XPCJSContext.cpp | 4 +- modules/libpref/init/all.js | 3 - xpcom/base/CycleCollectedJSContext.cpp | 4 ++ 15 files changed, 244 insertions(+), 56 deletions(-) diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index bf904e59fd..a3a190e1fc 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -301,7 +301,6 @@ LoadContextOptions(const char* aPrefName, void* /* aClosure */) .setAsyncStack(GetWorkerPref(NS_LITERAL_CSTRING("asyncstack"))) .setWerror(GetWorkerPref(NS_LITERAL_CSTRING("werror"))) .setStreams(GetWorkerPref(NS_LITERAL_CSTRING("streams"))) - .setWeakRefs(GetWorkerPref(NS_LITERAL_CSTRING("weakrefs"))) .setExtraWarnings(GetWorkerPref(NS_LITERAL_CSTRING("strict"))) .setArrayProtoValues(GetWorkerPref( NS_LITERAL_CSTRING("array_prototype_values"))); diff --git a/dom/workers/WorkerPrefs.h b/dom/workers/WorkerPrefs.h index d870ac8e46..87806a6fbc 100644 --- a/dom/workers/WorkerPrefs.h +++ b/dom/workers/WorkerPrefs.h @@ -35,7 +35,6 @@ WORKER_SIMPLE_PREF("dom.serviceWorkers.openWindow.enabled", OpenWindowEnabled, O WORKER_SIMPLE_PREF("dom.storageManager.enabled", StorageManagerEnabled, STORAGEMANAGER_ENABLED) WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED) WORKER_SIMPLE_PREF("dom.streams.enabled", StreamsEnabled, STREAMS_ENABLED) -WORKER_SIMPLE_PREF("javascript.options.weakrefs", WeakRefsEnabled, WEAKREFS_ENABLED) WORKER_SIMPLE_PREF("dom.requestcontext.enabled", RequestContextEnabled, REQUESTCONTEXT_ENABLED) WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED) WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK) diff --git a/js/src/builtin/WeakRefObject.cpp b/js/src/builtin/WeakRefObject.cpp index 856002dbb7..b929c65f5b 100644 --- a/js/src/builtin/WeakRefObject.cpp +++ b/js/src/builtin/WeakRefObject.cpp @@ -11,6 +11,7 @@ #include "gc/Nursery.h" #include "gc/Tracer.h" #include "vm/GlobalObject.h" +#include "vm/Symbol.h" #include "jsobjinlines.h" @@ -37,11 +38,34 @@ WeakRef_deref_impl(JSContext* cx, const CallArgs& args) MOZ_ASSERT(IsWeakRef(args.thisv())); WeakRefObject::Referent* data = GetReferent(&args.thisv().toObject()); - JSObject* target = data ? data->target.get() : nullptr; - if (target) - args.rval().setObject(*target); - else + if (!data) { args.rval().setUndefined(); + return true; + } + + if (data->isObject()) { + JSObject* target = data->objectTarget.get(); + if (target) { + RootedValue kept(cx, ObjectValue(*target)); + if (!cx->runtime()->addWeakRefKeptObject(cx, kept)) + return false; + args.rval().set(kept); + } else { + args.rval().setUndefined(); + } + } else { + MOZ_ASSERT(data->isSymbol()); + JS::Symbol* target = data->symbolTarget.get(); + if (target) { + RootedValue kept(cx, SymbolValue(target)); + if (!cx->runtime()->addWeakRefKeptObject(cx, kept)) + return false; + args.rval().set(kept); + } else { + args.rval().setUndefined(); + } + } + return true; } @@ -83,13 +107,18 @@ InitWeakRefClass(JSContext* cx, HandleObject obj, bool defineMembers) } /* static */ WeakRefObject* -WeakRefObject::create(JSContext* cx, HandleObject target, HandleObject proto /* = nullptr */) +WeakRefObject::create(JSContext* cx, HandleValue target, HandleObject proto /* = nullptr */) { Rooted obj(cx, NewObjectWithClassProto(cx, proto)); if (!obj) return nullptr; - Referent* data = cx->new_(target, cx->options().weakRefs()); + Referent* data; + if (target.isObject()) + data = cx->new_(&target.toObject()); + else + data = cx->new_(target.toSymbol()); + if (!data) return nullptr; @@ -97,6 +126,18 @@ WeakRefObject::create(JSContext* cx, HandleObject target, HandleObject proto /* return obj; } +static bool +CanBeHeldWeakly(HandleValue target) +{ + if (target.isObject()) + return true; + + if (target.isSymbol()) + return target.toSymbol()->code() != JS::SymbolCode::InSymbolRegistry; + + return false; +} + /* static */ bool WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) { @@ -105,18 +146,12 @@ WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) if (!ThrowIfNotConstructing(cx, args, "WeakRef")) return false; - if (!args.get(0).isObject()) { - UniqueChars bytes = - DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, args.get(0), nullptr); - if (!bytes) - return false; - - JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, - bytes.get()); + if (!CanBeHeldWeakly(args.get(0))) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_WEAKREF_TARGET); return false; } - RootedObject target(cx, &args[0].toObject()); + RootedValue target(cx, args[0]); RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); @@ -127,6 +162,9 @@ WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) if (!obj) return false; + if (!cx->runtime()->addWeakRefKeptObject(cx, target)) + return false; + args.rval().setObject(*obj); return true; } @@ -142,19 +180,23 @@ WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp) WeakRefObject::trace(JSTracer* trc, JSObject* obj) { if (Referent* data = GetReferent(obj)) { - JSObject* target = data->target.unbarrieredGet(); - if (!target) - return; + if (data->isObject()) { + JSObject* target = data->objectTarget.unbarrieredGet(); + if (!target) + return; - // When pref-disabled, keep referent alive via strong trace so deref() - // stays usable as a stub without touching GC internals. - if (!data->enabled) { - TraceManuallyBarrieredEdge(trc, data->target.unsafeGet(), "WeakRef stub referent"); - } else if (IsInsideNursery(target)) { - // Weak edges must be tenured; trace strongly while referent is in the nursery. - TraceManuallyBarrieredEdge(trc, data->target.unsafeGet(), "WeakRef nursery referent"); + if (IsInsideNursery(target)) { + // Weak edges must be tenured; trace strongly while referent is in the nursery. + TraceManuallyBarrieredEdge(trc, data->objectTarget.unsafeGet(), + "WeakRef nursery referent"); + } else { + TraceWeakEdge(trc, &data->objectTarget, "WeakRef referent"); + } } else { - TraceWeakEdge(trc, &data->target, "WeakRef referent"); + MOZ_ASSERT(data->isSymbol()); + JS::Symbol* target = data->symbolTarget.unbarrieredGet(); + if (target) + TraceWeakEdge(trc, &data->symbolTarget, "WeakRef symbol referent"); } } } @@ -162,7 +204,6 @@ WeakRefObject::trace(JSTracer* trc, JSObject* obj) /* static */ void WeakRefObject::finalize(FreeOp* fop, JSObject* obj) { - MOZ_ASSERT(!fop->maybeOffMainThread()); if (Referent* data = GetReferent(obj)) fop->delete_(data); } diff --git a/js/src/builtin/WeakRefObject.h b/js/src/builtin/WeakRefObject.h index f4d7e547e4..fc93b1e877 100644 --- a/js/src/builtin/WeakRefObject.h +++ b/js/src/builtin/WeakRefObject.h @@ -15,16 +15,28 @@ class WeakRefObject : public NativeObject { public: struct Referent { - explicit Referent(JSObject* obj, bool enabled) - : target(obj), enabled(enabled) {} - WeakRef target; - bool enabled; + enum class Kind { + Object, + Symbol + }; + + explicit Referent(JSObject* obj) + : kind(Kind::Object), objectTarget(obj), symbolTarget(nullptr) {} + explicit Referent(JS::Symbol* sym) + : kind(Kind::Symbol), objectTarget(nullptr), symbolTarget(sym) {} + + bool isObject() const { return kind == Kind::Object; } + bool isSymbol() const { return kind == Kind::Symbol; } + + Kind kind; + WeakRef objectTarget; + WeakRef symbolTarget; }; static const Class class_; static JSObject* initClass(JSContext* cx, HandleObject obj); - static WeakRefObject* create(JSContext* cx, HandleObject target, HandleObject proto = nullptr); + static WeakRefObject* create(JSContext* cx, HandleValue target, HandleObject proto = nullptr); static void trace(JSTracer* trc, JSObject* obj); static void finalize(FreeOp* fop, JSObject* obj); @@ -37,7 +49,8 @@ class WeakRefObject : public NativeObject WeakRef& target() { MOZ_ASSERT(getData()); - return getData()->target; + MOZ_ASSERT(getData()->isObject()); + return getData()->objectTarget; } static const JSPropertySpec properties[]; diff --git a/js/src/js.msg b/js/src/js.msg index c635bb54ed..4bb4d00497 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -81,6 +81,7 @@ MSG_DEF(JSMSG_EMPTY_ARRAY_REDUCE, 0, JSEXN_TYPEERR, "reduce of empty array MSG_DEF(JSMSG_UNEXPECTED_TYPE, 2, JSEXN_TYPEERR, "{0} is {1}") MSG_DEF(JSMSG_MISSING_FUN_ARG, 2, JSEXN_TYPEERR, "missing argument {0} when calling function {1}") MSG_DEF(JSMSG_NOT_NONNULL_OBJECT, 1, JSEXN_TYPEERR, "{0} is not a non-null object") +MSG_DEF(JSMSG_NOT_WEAKREF_TARGET, 0, JSEXN_TYPEERR, "WeakRef target must be an object or a non-registered symbol") MSG_DEF(JSMSG_SET_NON_OBJECT_RECEIVER, 1, JSEXN_TYPEERR, "can't assign to properties of {0}: not an object") MSG_DEF(JSMSG_INVALID_DESCRIPTOR, 0, JSEXN_TYPEERR, "property descriptors must not specify a value or be writable when a getter or setter has been specified") MSG_DEF(JSMSG_OBJECT_NOT_EXTENSIBLE, 1, JSEXN_TYPEERR, "{0}: Object is not extensible") diff --git a/js/src/jsapi-tests/testGCWeakRef.cpp b/js/src/jsapi-tests/testGCWeakRef.cpp index 11e6832d96..60cf71db87 100644 --- a/js/src/jsapi-tests/testGCWeakRef.cpp +++ b/js/src/jsapi-tests/testGCWeakRef.cpp @@ -21,6 +21,64 @@ struct MyHeap BEGIN_TEST(testGCWeakRef) { + CHECK(cx->options().weakRefs()); + cx->options().setWeakRefs(false); + CHECK(cx->options().weakRefs()); + + JS::RootedValue v(cx); + EXEC("var weakRefTarget = { x: 42 };\n" + "var weakRef = new WeakRef(weakRefTarget);\n" + "weakRefTarget = null;\n"); + + // The constructor keeps the target alive until the host clears kept + // objects. This must remain true even after callers try the old disabled + // option path above. + JS_GC(cx); + JS_GC(cx); + EVAL("weakRef.deref()", &v); + CHECK(v.isObject()); + + JS::ClearWeakRefKeptObjects(cx); + v = JS::UndefinedValue(); + JS_GC(cx); + JS_GC(cx); + EVAL("weakRef.deref()", &v); + CHECK(v.isUndefined()); + + EXEC("var weakRefSymbol = Symbol('weak-ref-symbol');\n" + "var symbolRef = new WeakRef(weakRefSymbol);\n" + "if (symbolRef.deref() !== weakRefSymbol)\n" + " throw new Error('WeakRef must accept unique symbols');\n" + "var registeredSymbolRejected = false;\n" + "try {\n" + " new WeakRef(Symbol.for('weak-ref-symbol'));\n" + "} catch (e) {\n" + " registeredSymbolRejected = e instanceof TypeError;\n" + "}\n" + "if (!registeredSymbolRejected)\n" + " throw new Error('WeakRef must reject registered symbols');\n"); + + EXEC("var keptTarget = { y: 7 };\n" + "var keptRef = new WeakRef(keptTarget);\n"); + JS::ClearWeakRefKeptObjects(cx); + EXEC("var keptResult = keptRef.deref();\n" + "if (keptResult !== keptTarget)\n" + " throw new Error('WeakRef deref must return the target');\n" + "keptTarget = null;\n" + "keptResult = null;\n"); + + JS_GC(cx); + JS_GC(cx); + EVAL("keptRef.deref()", &v); + CHECK(v.isObject()); + + JS::ClearWeakRefKeptObjects(cx); + v = JS::UndefinedValue(); + JS_GC(cx); + JS_GC(cx); + EVAL("keptRef.deref()", &v); + CHECK(v.isUndefined()); + // Create an object and add a property to it so that we can read the // property back later to verify that object internals are not garbage. JS::RootedObject obj(cx, JS_NewPlainObject(cx)); @@ -38,7 +96,6 @@ BEGIN_TEST(testGCWeakRef) // references. CHECK(heap.get().weak.unbarrieredGet() != nullptr); obj = heap.get().weak; - JS::RootedValue v(cx); CHECK(JS_GetProperty(cx, obj, "x", &v)); CHECK(v.isInt32()); CHECK(v.toInt32() == 42); @@ -61,4 +118,3 @@ BEGIN_TEST(testGCWeakRef) return true; } END_TEST(testGCWeakRef) - diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 5edb7cc5be..93164c7c5e 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -627,6 +627,15 @@ JS::InitSelfHostedCode(JSContext* cx) return true; } +JS_PUBLIC_API(void) +JS::ClearWeakRefKeptObjects(JSContext* cx) +{ + MOZ_ASSERT(cx); + MOZ_ASSERT(!cx->runtime()->isHeapBusy()); + + cx->runtime()->clearWeakRefKeptObjects(); +} + JS_PUBLIC_API(const char*) JS_GetImplementationVersion(void) { diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 969ee4cd19..0f033fef37 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -999,7 +999,7 @@ class JS_PUBLIC_API(ContextOptions) { strictMode_(false), extraWarnings_(false), arrayProtoValues_(true), - weakRefs_(false) + streams_(false) { } @@ -1139,13 +1139,12 @@ class JS_PUBLIC_API(ContextOptions) { return *this; } - bool weakRefs() const { return weakRefs_; } + bool weakRefs() const { return true; } ContextOptions& setWeakRefs(bool flag) { - weakRefs_ = flag; + (void) flag; return *this; } ContextOptions& toggleWeakRefs() { - weakRefs_ = !weakRefs_; return *this; } @@ -1166,7 +1165,6 @@ class JS_PUBLIC_API(ContextOptions) { bool extraWarnings_ : 1; bool arrayProtoValues_ : 1; bool streams_ : 1; - bool weakRefs_ : 1; }; JS_PUBLIC_API(ContextOptions&) @@ -1187,6 +1185,13 @@ InitSelfHostedCode(JSContext* cx); JS_PUBLIC_API(void) AssertObjectBelongsToCurrentThread(JSObject* obj); +/** + * Clear objects and symbols that WeakRef.prototype.deref kept alive for the + * current synchronous JavaScript execution. + */ +JS_PUBLIC_API(void) +ClearWeakRefKeptObjects(JSContext* cx); + } /* namespace JS */ extern JS_PUBLIC_API(const char*) diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index d3463a9ff5..1e4ee792fc 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -676,7 +676,9 @@ RunFile(JSContext* cx, const char* filename, FILE* file, bool compileOnly) AnalyzeEntrainedVariables(cx, script); #endif if (!compileOnly) { - if (!JS_ExecuteScript(cx, script)) + bool ok = JS_ExecuteScript(cx, script); + JS::ClearWeakRefKeptObjects(cx); + if (!ok) return false; int64_t t2 = PRMJ_Now() - t1; if (printTiming) @@ -857,6 +859,7 @@ DrainJobQueue(JSContext* cx) } sc->jobQueue.clear(); sc->drainingJobQueue = false; + JS::ClearWeakRefKeptObjects(cx); return true; } diff --git a/js/src/tests/manual/weakref-smoke.js b/js/src/tests/manual/weakref-smoke.js index da959467bc..e6dfa9b822 100644 --- a/js/src/tests/manual/weakref-smoke.js +++ b/js/src/tests/manual/weakref-smoke.js @@ -1,15 +1,13 @@ // Manual WeakRef smoke tests for browser console use. // // Usage: -// 1) Optionally flip `javascript.options.weakrefs` in about:config and reload. -// 2) Paste this file into the browser console (or load via file://) and run: +// 1) Paste this file into the browser console (or load via file://) and run: // runWeakRefManual(); -// 3) Inspect the returned array of results; no throws or crashes are expected. +// 2) Inspect the returned array of results; no throws or crashes are expected. // // Notes: -// - When the pref is ON, deref() should return the target. -// - When the pref is OFF, the stub traces strongly so deref() should also -// return the target. +// - WeakRef is always enabled. Once all strong references are gone, a full GC +// may clear the referent and make deref() return undefined. function runWeakRefManual() { const results = []; @@ -21,7 +19,7 @@ function runWeakRefManual() { log("initial deref tag", ref.deref()?.tag); log("repeat deref identity", ref.deref() === target); - // Clear the strong reference; the WeakRef should still be able to return it. + // Clear the strong reference. A future full GC may clear the WeakRef. target = null; const afterClear = ref.deref(); diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 969c161713..3ab5f50ccd 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -194,6 +194,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime) simulator_(nullptr), #endif scriptAndCountsVector(nullptr), + weakRefKeptObjects(nullptr), lcovOutput(), NaNValue(DoubleNaNValue()), negativeInfinityValue(DoubleValue(NegativeInfinity())), @@ -372,6 +373,8 @@ JSRuntime::destroyRuntime() MOZ_ASSERT(!isHeapBusy()); MOZ_ASSERT(childRuntimeCount == 0); + clearWeakRefKeptObjects(); + fx.destroyInstance(); sharedIntlData.destroyInstance(); @@ -460,6 +463,61 @@ JSRuntime::destroyRuntime() #endif } +static bool +SameWeakRefKeptObject(const JS::Value& kept, JS::HandleValue target) +{ + MOZ_ASSERT(kept.isObject() || kept.isSymbol()); + MOZ_ASSERT(target.isObject() || target.isSymbol()); + + if (kept.isObject()) + return target.isObject() && &kept.toObject() == &target.toObject(); + + return target.isSymbol() && kept.toSymbol() == target.toSymbol(); +} + +bool +JSRuntime::addWeakRefKeptObject(JSContext* cx, JS::HandleValue target) +{ + MOZ_ASSERT(cx->runtime() == this); + MOZ_ASSERT(target.isObject() || target.isSymbol()); + MOZ_ASSERT(!isHeapBusy()); + + if (!weakRefKeptObjects) { + auto* keptObjects = + cx->new_>( + cx, js::WeakRefKeptObjectVector(js::SystemAllocPolicy())); + if (!keptObjects) + return false; + + weakRefKeptObjects = keptObjects; + } + + for (size_t i = 0; i < weakRefKeptObjects->length(); i++) { + const JS::Value& kept = (*weakRefKeptObjects)[i]; + if (SameWeakRefKeptObject(kept, target)) + return true; + } + + if (!weakRefKeptObjects->append(target.get())) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +void +JSRuntime::clearWeakRefKeptObjects() +{ + MOZ_ASSERT(!isHeapBusy()); + + if (!weakRefKeptObjects) + return; + + defaultFreeOp()->delete_(weakRefKeptObjects); + weakRefKeptObjects = nullptr; +} + void JSRuntime::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, JS::RuntimeSizes* rtSizes) { diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 32ea35bc02..b31b8f2cb3 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -360,6 +360,7 @@ class PerThreadData }; using ScriptAndCountsVector = GCVector; +using WeakRefKeptObjectVector = JS::GCVector; class AutoLockForExclusiveAccess; } // namespace js @@ -887,6 +888,12 @@ struct JSRuntime : public JS::shadow::Runtime, /* Strong references on scripts held for PCCount profiling API. */ JS::PersistentRooted* scriptAndCountsVector; + /* Strong references to live WeakRef targets kept until the next job boundary. */ + JS::PersistentRooted* weakRefKeptObjects; + + [[nodiscard]] bool addWeakRefKeptObject(JSContext* cx, JS::HandleValue target); + void clearWeakRefKeptObjects(); + /* Code coverage output. */ js::coverage::LCovRuntime lcovOutput; diff --git a/js/xpconnect/src/XPCJSContext.cpp b/js/xpconnect/src/XPCJSContext.cpp index ad288cbd80..e93854a9f9 100644 --- a/js/xpconnect/src/XPCJSContext.cpp +++ b/js/xpconnect/src/XPCJSContext.cpp @@ -1442,7 +1442,6 @@ ReloadPrefsCallback(const char* pref, void* data) bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict"); bool streams = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams"); - bool weakRefs = Preferences::GetBool(JS_OPTIONS_DOT_STR "weakrefs"); bool unboxedObjects = Preferences::GetBool(JS_OPTIONS_DOT_STR "unboxed_objects"); @@ -1469,8 +1468,7 @@ ReloadPrefsCallback(const char* pref, void* data) .setWerror(werror) .setExtraWarnings(extraWarnings) .setArrayProtoValues(arrayProtoValues) - .setStreams(streams) - .setWeakRefs(weakRefs); + .setStreams(streams); JS_SetParallelParsingEnabled(cx, parallelParsing); JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 5f04312b54..e0c605f5ce 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1340,9 +1340,6 @@ pref("javascript.options.dynamicImport", true); // Streams API pref("javascript.options.streams", true); -// Enable garbage collection of weakrefed objects -pref("javascript.options.weakrefs", false); - // advanced prefs pref("advanced.mailftp", false); pref("image.animation_mode", "normal"); diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp index 18f6e8f6ca..841e417fc9 100644 --- a/xpcom/base/CycleCollectedJSContext.cpp +++ b/xpcom/base/CycleCollectedJSContext.cpp @@ -1479,6 +1479,8 @@ CycleCollectedJSContext::AfterProcessTask(uint32_t aRecursionDepth) // Step 4.2 Execute any events that were waiting for a stable state. ProcessStableStateQueue(); + + JS::ClearWeakRefKeptObjects(mJSContext); } void @@ -1517,6 +1519,8 @@ CycleCollectedJSContext::AfterProcessMicrotasks() }); RunInStableState(cleanupRunnable.forget()); }; + + JS::ClearWeakRefKeptObjects(mJSContext); } uint32_t From 50c1419e7550bd8224e33432ee0ca676e5a855ad Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 17:16:03 -0400 Subject: [PATCH 17/19] Implement FinalizationRegistry --- js/src/builtin/FinalizationRegistryObject.cpp | 584 ++++++++++++++++++ js/src/builtin/FinalizationRegistryObject.h | 51 ++ js/src/builtin/WeakRefObject.cpp | 4 +- js/src/builtin/WeakRefObject.h | 2 + js/src/gc/GCRuntime.h | 3 + js/src/gc/Zone.h | 5 + js/src/js.msg | 3 + js/src/jsgc.cpp | 56 ++ js/src/jsprototypes.h | 1 + js/src/moz.build | 1 + js/src/vm/GlobalObject.cpp | 2 + js/src/vm/Runtime.cpp | 70 +++ js/src/vm/Runtime.h | 8 + .../tests/unit/test_finalization_registry.js | 95 +++ js/xpconnect/tests/unit/xpcshell.ini | 1 + 15 files changed, 884 insertions(+), 2 deletions(-) create mode 100644 js/src/builtin/FinalizationRegistryObject.cpp create mode 100644 js/src/builtin/FinalizationRegistryObject.h create mode 100644 js/xpconnect/tests/unit/test_finalization_registry.js diff --git a/js/src/builtin/FinalizationRegistryObject.cpp b/js/src/builtin/FinalizationRegistryObject.cpp new file mode 100644 index 0000000000..e2a67efd42 --- /dev/null +++ b/js/src/builtin/FinalizationRegistryObject.cpp @@ -0,0 +1,584 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/FinalizationRegistryObject.h" + +#include "jsapi.h" +#include "jscntxt.h" + +#include "builtin/WeakRefObject.h" +#include "gc/Nursery.h" +#include "gc/Tracer.h" +#include "vm/EqualityOperations.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/Symbol.h" + +#include "jswrapper.h" +#include "jsobjinlines.h" + +#include "vm/Interpreter-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +enum class WeakCellKind { + Empty, + Object, + Symbol +}; + +static JSObject* +NormalizeWeakObject(JSObject* obj) +{ + if (JSObject* unwrapped = CheckedUnwrap(obj, /* stopAtWindowProxy = */ false)) + return unwrapped; + return obj; +} + +struct WeakCell { + WeakCellKind kind; + WeakRef object; + WeakRef symbol; + + WeakCell() : kind(WeakCellKind::Empty), object(nullptr), symbol(nullptr) {} + explicit WeakCell(JSObject* obj) : kind(WeakCellKind::Object), object(obj), symbol(nullptr) {} + explicit WeakCell(JS::Symbol* sym) : kind(WeakCellKind::Symbol), object(nullptr), symbol(sym) {} + + void clear() { + kind = WeakCellKind::Empty; + object = nullptr; + symbol = nullptr; + } + + bool isEmpty() const { return kind == WeakCellKind::Empty; } + + bool isDead() const { + if (kind == WeakCellKind::Object) + return !object.unbarrieredGet(); + if (kind == WeakCellKind::Symbol) + return !symbol.unbarrieredGet(); + return false; + } + + bool sameValue(HandleValue value) const { + if (kind == WeakCellKind::Object) + return value.isObject() && object.get() == NormalizeWeakObject(&value.toObject()); + if (kind == WeakCellKind::Symbol) + return value.isSymbol() && symbol.get() == value.toSymbol(); + return false; + } + + void trace(JSTracer* trc, const char* name) { + if (kind == WeakCellKind::Object) { + JSObject* target = object.unbarrieredGet(); + if (!target) + return; + + if (IsInsideNursery(target)) { + TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name); + } else { + if (trc->isMarkingTracer() && !target->asTenured().zone()->isCollecting()) + return; + TraceWeakEdge(trc, &object, name); + } + } else if (kind == WeakCellKind::Symbol) { + JS::Symbol* target = symbol.unbarrieredGet(); + if (target) { + if (trc->isMarkingTracer() && !target->asTenured().zone()->isCollecting()) + return; + TraceWeakEdge(trc, &symbol, name); + } + } + } + + void traceIfZoneIsCollecting(JSTracer* trc, const char* name) { + if (kind == WeakCellKind::Object) { + JSObject* target = object.unbarrieredGet(); + if (!target) + return; + + if (IsInsideNursery(target)) { + TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name); + } else if (target->asTenured().zone()->isCollecting()) { + TraceWeakEdge(trc, &object, name); + } + } else if (kind == WeakCellKind::Symbol) { + JS::Symbol* target = symbol.unbarrieredGet(); + if (target && target->asTenured().zone()->isCollecting()) + TraceWeakEdge(trc, &symbol, name); + } + } +}; + +struct FinalizationRecord { + WeakCell target; + JS::Heap heldValue; + WeakCell unregisterToken; + bool active; + bool queued; + + FinalizationRecord(HandleValue targetValue, HandleValue heldValue, + HandleValue unregisterTokenValue) + : heldValue(heldValue), + active(true), + queued(false) + { + if (targetValue.isObject()) + target = WeakCell(NormalizeWeakObject(&targetValue.toObject())); + else + target = WeakCell(targetValue.toSymbol()); + + if (unregisterTokenValue.isObject()) + unregisterToken = WeakCell(NormalizeWeakObject(&unregisterTokenValue.toObject())); + else if (unregisterTokenValue.isSymbol()) + unregisterToken = WeakCell(unregisterTokenValue.toSymbol()); + } + + void trace(JSTracer* trc) { + if (active || queued) + JS::TraceEdge(trc, &heldValue, "FinalizationRegistry held value"); + + if (active) { + target.trace(trc, "FinalizationRegistry target"); + if (!unregisterToken.isEmpty()) + unregisterToken.trace(trc, "FinalizationRegistry unregister token"); + } + } + + void traceWeakEdgesForCollectedZones(JSTracer* trc) { + if (!active) + return; + + target.traceIfZoneIsCollecting(trc, "FinalizationRegistry target"); + if (!unregisterToken.isEmpty()) + unregisterToken.traceIfZoneIsCollecting(trc, + "FinalizationRegistry unregister token"); + } + + bool targetIsDead() const { + return active && target.isDead(); + } + + bool matchesToken(HandleValue token) const { + return active && !queued && !unregisterToken.isEmpty() && + unregisterToken.sameValue(token); + } + + void queueForCleanup() { + MOZ_ASSERT(active); + MOZ_ASSERT(!queued); + active = false; + queued = true; + target.clear(); + unregisterToken.clear(); + } + + void clear() { + active = false; + queued = false; + target.clear(); + unregisterToken.clear(); + heldValue.setUndefined(); + } +}; + +using FinalizationRecordVector = Vector; + +struct FinalizationRegistryObject::Data { + JS::Heap cleanupCallback; + JS::Heap cleanupJob; + FinalizationRecordVector records; + FinalizationRecordVector cleanupQueue; + bool queuedForCleanup; + + explicit Data(JSObject* cleanupCallback) + : cleanupCallback(cleanupCallback), + cleanupJob(nullptr), + records(SystemAllocPolicy()), + cleanupQueue(SystemAllocPolicy()), + queuedForCleanup(false) + {} + + ~Data() { + for (size_t i = 0; i < records.length(); i++) + js_delete(records[i]); + } + + void trace(JSTracer* trc) { + JS::TraceEdge(trc, &cleanupCallback, "FinalizationRegistry cleanup callback"); + if (cleanupJob.unbarrieredGet()) + JS::TraceEdge(trc, &cleanupJob, "FinalizationRegistry cleanup job"); + for (size_t i = 0; i < records.length(); i++) + records[i]->trace(trc); + } + + bool appendRecord(FinalizationRecord* record) { + return records.append(record); + } + + bool appendCleanupRecord(FinalizationRecord* record) { + return cleanupQueue.append(record); + } + + void compactRecords() { + for (size_t i = 0; i < records.length();) { + FinalizationRecord* record = records[i]; + if (!record->active && !record->queued) { + js_delete(record); + records.erase(records.begin() + i); + continue; + } + i++; + } + } + + void unregister(HandleValue token, bool* removed) { + *removed = false; + for (size_t i = 0; i < records.length(); i++) { + FinalizationRecord* record = records[i]; + if (record->matchesToken(token)) { + record->clear(); + *removed = true; + } + } + compactRecords(); + } +}; + +static FinalizationRegistryObject::Data* +GetData(JSObject* obj) +{ + return obj->as().getData(); +} + +static MOZ_ALWAYS_INLINE bool +IsFinalizationRegistry(HandleValue v) +{ + return v.isObject() && v.toObject().is(); +} + +static bool +FinalizationRegistry_register_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsFinalizationRegistry(args.thisv())); + + if (!CanBeHeldWeakly(args.get(0))) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_FINALIZATION_REGISTRY_TARGET); + return false; + } + + RootedValue target(cx, args[0]); + RootedValue heldValue(cx, args.get(1)); + + bool same; + if (!SameValue(cx, target, heldValue, &same)) + return false; + if (same) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_FINALIZATION_REGISTRY_HELD_VALUE); + return false; + } + + RootedValue unregisterToken(cx, args.get(2)); + if (!CanBeHeldWeakly(unregisterToken)) { + if (!unregisterToken.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_FINALIZATION_REGISTRY_TOKEN); + return false; + } + unregisterToken.setUndefined(); + } + + FinalizationRecord* record = cx->new_(target, heldValue, unregisterToken); + if (!record) + return false; + + auto* data = GetData(&args.thisv().toObject()); + if (!data->appendRecord(record)) { + js_delete(record); + ReportOutOfMemory(cx); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool +FinalizationRegistry_unregister_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsFinalizationRegistry(args.thisv())); + + RootedValue unregisterToken(cx, args.get(0)); + if (!CanBeHeldWeakly(unregisterToken)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_FINALIZATION_REGISTRY_TOKEN); + return false; + } + + bool removed = false; + auto* data = GetData(&args.thisv().toObject()); + data->unregister(unregisterToken, &removed); + + args.rval().setBoolean(removed); + return true; +} + +const JSPropertySpec FinalizationRegistryObject::properties[] = { + JS_PS_END +}; + +const JSFunctionSpec FinalizationRegistryObject::methods[] = { + JS_FN("register", FinalizationRegistryObject::register_, 2, 0), + JS_FN("unregister", FinalizationRegistryObject::unregister, 1, 0), + JS_FS_END +}; + +static JSObject* +InitFinalizationRegistryClass(JSContext* cx, HandleObject obj, bool defineMembers) +{ + Handle global = obj.as(); + RootedPlainObject proto(cx, NewBuiltinClassInstance(cx)); + if (!proto) + return nullptr; + + RootedFunction ctor(cx, GlobalObject::createConstructor(cx, FinalizationRegistryObject::construct, + ClassName(JSProto_FinalizationRegistry, cx), 1)); + if (!ctor) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (defineMembers) { + if (!DefinePropertiesAndFunctions(cx, proto, FinalizationRegistryObject::properties, + FinalizationRegistryObject::methods)) { + return nullptr; + } + if (!DefineToStringTag(cx, proto, cx->names().FinalizationRegistry)) + return nullptr; + } + + if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_FinalizationRegistry, ctor, proto)) + return nullptr; + return proto; +} + +/* static */ FinalizationRegistryObject* +FinalizationRegistryObject::create(JSContext* cx, HandleObject cleanupCallback, + HandleObject proto /* = nullptr */) +{ + Rooted obj(cx, + NewObjectWithClassProto(cx, proto)); + if (!obj) + return nullptr; + + Data* data = cx->new_(cleanupCallback); + if (!data) + return nullptr; + + obj->setPrivate(data); + + RootedAtom funName(cx, cx->names().empty); + RootedFunction cleanupJob(cx, NewNativeFunction(cx, FinalizationRegistryObject::cleanupJob, 0, + funName, gc::AllocKind::FUNCTION_EXTENDED, + GenericObject)); + if (!cleanupJob) + return nullptr; + + cleanupJob->setExtendedSlot(0, ObjectValue(*obj)); + data->cleanupJob = cleanupJob; + + if (!cx->zone()->finalizationRegistries.append(WeakRef(obj))) { + ReportOutOfMemory(cx); + return nullptr; + } + + return obj; +} + +/* static */ bool +FinalizationRegistryObject::construct(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "FinalizationRegistry")) + return false; + + RootedValue cleanupCallbackValue(cx, args.get(0)); + RootedObject cleanupCallback(cx, ValueToCallable(cx, cleanupCallbackValue, 0, NO_CONSTRUCT)); + if (!cleanupCallback) + return false; + + RootedObject proto(cx); + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + return false; + + Rooted obj(cx, + FinalizationRegistryObject::create(cx, cleanupCallback, proto)); + if (!obj) + return false; + + args.rval().setObject(*obj); + return true; +} + +/* static */ bool +FinalizationRegistryObject::register_(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +/* static */ bool +FinalizationRegistryObject::unregister(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +/* static */ bool +FinalizationRegistryObject::cleanupJob(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedFunction job(cx, &args.callee().as()); + Rooted registry(cx, + &job->getExtendedSlot(0).toObject().as()); + Data* data = registry->getData(); + if (!data) { + args.rval().setUndefined(); + return true; + } + + data->queuedForCleanup = false; + + RootedValue callback(cx, ObjectValue(*data->cleanupCallback.get())); + RootedValue heldValue(cx); + RootedValue ignored(cx); + RootedValue undefined(cx, UndefinedValue()); + + while (data->cleanupQueue.length() > 0) { + FinalizationRecord* record = data->cleanupQueue[0]; + data->cleanupQueue.erase(data->cleanupQueue.begin()); + + if (!record->queued) { + data->compactRecords(); + continue; + } + + heldValue.set(record->heldValue.get()); + record->clear(); + data->compactRecords(); + + if (!Call(cx, callback, undefined, heldValue, &ignored)) + return false; + } + + args.rval().setUndefined(); + return true; +} + +/* static */ void +FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj) +{ + if (Data* data = GetData(obj)) + data->trace(trc); +} + +/* static */ void +FinalizationRegistryObject::finalize(FreeOp* fop, JSObject* obj) +{ + if (Data* data = GetData(obj)) + fop->delete_(data); +} + +void +FinalizationRegistryObject::traceWeakEdgesForCollectedZones(JSTracer* trc) +{ + Data* data = getData(); + if (!data) + return; + + for (size_t i = 0; i < data->records.length(); i++) + data->records[i]->traceWeakEdgesForCollectedZones(trc); +} + +void +FinalizationRegistryObject::sweepAfterGC(JSRuntime* rt) +{ + Data* data = getData(); + if (!data) + return; + + bool needsCleanupJob = false; + for (size_t i = 0; i < data->records.length(); i++) { + FinalizationRecord* record = data->records[i]; + if (!record->targetIsDead()) + continue; + + record->queueForCleanup(); + if (!data->appendCleanupRecord(record)) { + AutoEnterOOMUnsafeRegion oomUnsafe; + oomUnsafe.crash("queueing FinalizationRegistry cleanup record"); + } + needsCleanupJob = true; + } + + if (needsCleanupJob && !data->queuedForCleanup) { + JSContext* cx = rt->contextFromMainThread(); + RootedObject cleanupJob(cx, data->cleanupJob.unbarrieredGet()); + if (!rt->enqueueFinalizationRegistryCleanupJob(cx, cleanupJob)) { + AutoEnterOOMUnsafeRegion oomUnsafe; + oomUnsafe.crash("queueing FinalizationRegistry cleanup job"); + } + data->queuedForCleanup = true; + } + + data->compactRecords(); +} + +static const ClassOps FinalizationRegistryObjectClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + FinalizationRegistryObject::finalize, + nullptr, /* call */ + nullptr, /* hasInstance */ + nullptr, /* construct */ + FinalizationRegistryObject::trace +}; + +const Class FinalizationRegistryObject::class_ = { + "FinalizationRegistry", + JSCLASS_HAS_PRIVATE | + JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry) | + JSCLASS_FOREGROUND_FINALIZE, + &FinalizationRegistryObjectClassOps +}; + +/* static */ JSObject* +FinalizationRegistryObject::initClass(JSContext* cx, HandleObject obj) +{ + return ::InitFinalizationRegistryClass(cx, obj, true); +} + +JSObject* +js::InitFinalizationRegistryClass(JSContext* cx, HandleObject obj) +{ + return FinalizationRegistryObject::initClass(cx, obj); +} + +JSObject* +js::InitBareFinalizationRegistryCtor(JSContext* cx, HandleObject obj) +{ + return ::InitFinalizationRegistryClass(cx, obj, false); +} diff --git a/js/src/builtin/FinalizationRegistryObject.h b/js/src/builtin/FinalizationRegistryObject.h new file mode 100644 index 0000000000..afb21afd4c --- /dev/null +++ b/js/src/builtin/FinalizationRegistryObject.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_FinalizationRegistryObject_h +#define builtin_FinalizationRegistryObject_h + +#include "gc/Barrier.h" +#include "vm/NativeObject.h" + +namespace js { + +class FinalizationRegistryObject : public NativeObject +{ + public: + struct Data; + + static const Class class_; + + static JSObject* initClass(JSContext* cx, HandleObject obj); + static FinalizationRegistryObject* create(JSContext* cx, HandleObject cleanupCallback, + HandleObject proto = nullptr); + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(FreeOp* fop, JSObject* obj); + void traceWeakEdgesForCollectedZones(JSTracer* trc); + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool register_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool unregister(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool cleanupJob(JSContext* cx, unsigned argc, Value* vp); + + void sweepAfterGC(JSRuntime* rt); + + Data* getData() const { + return static_cast(getPrivate()); + } + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; +}; + +extern JSObject* +InitFinalizationRegistryClass(JSContext* cx, HandleObject obj); + +extern JSObject* +InitBareFinalizationRegistryCtor(JSContext* cx, HandleObject obj); + +} // namespace js + +#endif /* builtin_FinalizationRegistryObject_h */ diff --git a/js/src/builtin/WeakRefObject.cpp b/js/src/builtin/WeakRefObject.cpp index b929c65f5b..ed92f547d7 100644 --- a/js/src/builtin/WeakRefObject.cpp +++ b/js/src/builtin/WeakRefObject.cpp @@ -126,8 +126,8 @@ WeakRefObject::create(JSContext* cx, HandleValue target, HandleObject proto /* = return obj; } -static bool -CanBeHeldWeakly(HandleValue target) +bool +js::CanBeHeldWeakly(HandleValue target) { if (target.isObject()) return true; diff --git a/js/src/builtin/WeakRefObject.h b/js/src/builtin/WeakRefObject.h index fc93b1e877..3bf6d6f96b 100644 --- a/js/src/builtin/WeakRefObject.h +++ b/js/src/builtin/WeakRefObject.h @@ -11,6 +11,8 @@ namespace js { +bool CanBeHeldWeakly(HandleValue target); + class WeakRefObject : public NativeObject { public: diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index 89da938017..ef0ca7072d 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -947,6 +947,8 @@ class GCRuntime IncrementalProgress drainMarkStack(SliceBudget& sliceBudget, gcstats::Phase phase); template void markWeakReferences(gcstats::Phase phase); void markWeakReferencesInCurrentGroup(gcstats::Phase phase); + template void traceFinalizationRegistryWeakRefs(); + void traceFinalizationRegistryWeakRefsInCurrentGroup(); template void markGrayReferences(gcstats::Phase phase); void markBufferedGrayRoots(JS::Zone* zone); void markGrayReferencesInCurrentGroup(gcstats::Phase phase); @@ -959,6 +961,7 @@ class GCRuntime void getNextZoneGroup(); void endMarkingZoneGroup(); void beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock); + void sweepFinalizationRegistries(); bool shouldReleaseObservedTypes(); void endSweepingZoneGroup(); IncrementalProgress performSweepActions(SliceBudget& sliceBudget, AutoLockForExclusiveAccess& lock); diff --git a/js/src/gc/Zone.h b/js/src/gc/Zone.h index a11c9603ab..86519fe189 100644 --- a/js/src/gc/Zone.h +++ b/js/src/gc/Zone.h @@ -12,6 +12,7 @@ #include "jscntxt.h" #include "ds/SplayTree.h" +#include "gc/Barrier.h" #include "gc/FindSCCs.h" #include "gc/GCRuntime.h" #include "js/GCHashTable.h" @@ -325,6 +326,10 @@ struct Zone : public JS::shadow::Zone, using WeakEdges = js::Vector; WeakEdges gcWeakRefs; + // FinalizationRegistry objects with weakly-held target cells in this zone. + using FinalizationRegistryVector = js::Vector, 0, js::SystemAllocPolicy>; + FinalizationRegistryVector finalizationRegistries; + // List of non-ephemeron weak containers to sweep during beginSweepingZoneGroup. mozilla::LinkedList> weakCaches_; void registerWeakCache(WeakCache* cachep) { diff --git a/js/src/js.msg b/js/src/js.msg index 4bb4d00497..967d075449 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -82,6 +82,9 @@ MSG_DEF(JSMSG_UNEXPECTED_TYPE, 2, JSEXN_TYPEERR, "{0} is {1}") MSG_DEF(JSMSG_MISSING_FUN_ARG, 2, JSEXN_TYPEERR, "missing argument {0} when calling function {1}") MSG_DEF(JSMSG_NOT_NONNULL_OBJECT, 1, JSEXN_TYPEERR, "{0} is not a non-null object") MSG_DEF(JSMSG_NOT_WEAKREF_TARGET, 0, JSEXN_TYPEERR, "WeakRef target must be an object or a non-registered symbol") +MSG_DEF(JSMSG_BAD_FINALIZATION_REGISTRY_TARGET, 0, JSEXN_TYPEERR, "FinalizationRegistry target must be an object or a non-registered symbol") +MSG_DEF(JSMSG_BAD_FINALIZATION_REGISTRY_HELD_VALUE, 0, JSEXN_TYPEERR, "FinalizationRegistry held value must not be the target") +MSG_DEF(JSMSG_BAD_FINALIZATION_REGISTRY_TOKEN, 0, JSEXN_TYPEERR, "FinalizationRegistry unregister token must be an object or a non-registered symbol") MSG_DEF(JSMSG_SET_NON_OBJECT_RECEIVER, 1, JSEXN_TYPEERR, "can't assign to properties of {0}: not an object") MSG_DEF(JSMSG_INVALID_DESCRIPTOR, 0, JSEXN_TYPEERR, "property descriptors must not specify a value or be writable when a getter or setter has been specified") MSG_DEF(JSMSG_OBJECT_NOT_EXTENSIBLE, 1, JSEXN_TYPEERR, "{0}: Object is not extensible") diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 48cd51f3b5..68fcd46770 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -211,6 +211,7 @@ # include "jswin.h" #endif +#include "builtin/FinalizationRegistryObject.h" #include "gc/FindSCCs.h" #include "gc/GCInternals.h" #include "gc/GCTrace.h" @@ -4029,6 +4030,8 @@ GCRuntime::markWeakReferences(gcstats::Phase phase) } MOZ_ASSERT(marker.isDrained()); + traceFinalizationRegistryWeakRefs(); + marker.leaveWeakMarkingMode(); } @@ -4038,6 +4041,33 @@ GCRuntime::markWeakReferencesInCurrentGroup(gcstats::Phase phase) markWeakReferences(phase); } +template +void +GCRuntime::traceFinalizationRegistryWeakRefs() +{ + for (ZoneIterT zone(rt); !zone.done(); zone.next()) { + for (size_t i = 0; i < zone->finalizationRegistries.length(); i++) { + WeakRef& registry = zone->finalizationRegistries[i]; + if (registry.unbarrieredGet()) + TraceWeakEdge(&marker, ®istry, "FinalizationRegistry registry"); + } + } + + for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) { + for (size_t i = 0; i < zone->finalizationRegistries.length(); i++) { + JSObject* registry = zone->finalizationRegistries[i].unbarrieredGet(); + if (registry && registry->is()) + registry->as().traceWeakEdgesForCollectedZones(&marker); + } + } +} + +void +GCRuntime::traceFinalizationRegistryWeakRefsInCurrentGroup() +{ + traceFinalizationRegistryWeakRefs(); +} + template void GCRuntime::markGrayReferences(gcstats::Phase phase) @@ -4749,6 +4779,8 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) oomUnsafe.crash("clearing weak keys in beginSweepingZoneGroup()"); } + sweepFinalizationRegistries(); + { gcstats::AutoPhase ap(stats, gcstats::PHASE_FINALIZE_START); callFinalizeCallbacks(&fop, JSFINALIZE_GROUP_START); @@ -4904,6 +4936,23 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) } } +void +GCRuntime::sweepFinalizationRegistries() +{ + for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) { + for (size_t i = 0; i < zone->finalizationRegistries.length();) { + JSObject* obj = zone->finalizationRegistries[i].unbarrieredGet(); + if (!obj || !obj->is()) { + zone->finalizationRegistries.erase(zone->finalizationRegistries.begin() + i); + continue; + } + + obj->as().sweepAfterGC(rt); + i++; + } + } +} + void GCRuntime::endSweepingZoneGroup() { @@ -6095,6 +6144,13 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R repeat = (poked && cleanUpEverything) || wasReset || repeatForDeadZone; } while (repeat); + if (rt->isBeingDestroyed()) { + rt->clearFinalizationRegistryCleanupJobs(); + } else if (!rt->drainFinalizationRegistryCleanupJobs(rt->contextFromMainThread())) { + AutoEnterOOMUnsafeRegion oomUnsafe; + oomUnsafe.crash("draining FinalizationRegistry cleanup jobs"); + } + if (reason == JS::gcreason::COMPARTMENT_REVIVED) maybeDoCycleCollection(); } diff --git a/js/src/jsprototypes.h b/js/src/jsprototypes.h index 6005724f89..97b8981f2f 100644 --- a/js/src/jsprototypes.h +++ b/js/src/jsprototypes.h @@ -97,6 +97,7 @@ IF_SAB(real,imaginary)(SharedArrayBuffer, InitViaClassSpec, OCLASP(SharedArrayBuffer)) \ IF_INTL(real,imaginary) (Intl, InitIntlClass, CLASP(Intl)) \ IF_BDATA(real,imaginary)(TypedObject, InitTypedObjectModuleObject, OCLASP(TypedObjectModule)) \ + real(FinalizationRegistry, InitFinalizationRegistryClass, OCLASP(FinalizationRegistry)) \ real(Reflect, InitReflect, nullptr) \ real(WeakSet, InitWeakSetClass, OCLASP(WeakSet)) \ real(TypedArray, InitViaClassSpec, &js::TypedArrayObject::sharedTypedArrayPrototypeClass) \ diff --git a/js/src/moz.build b/js/src/moz.build index 500d00caac..ede829145b 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -120,6 +120,7 @@ EXPORTS.js += [ main_deunified_sources = [ 'builtin/AtomicsObject.cpp', 'builtin/Eval.cpp', + 'builtin/FinalizationRegistryObject.cpp', 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/DateTimeFormat.cpp', diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 1625820959..cf6e5f5210 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -17,6 +17,7 @@ #include "builtin/AtomicsObject.h" #include "builtin/BigInt.h" #include "builtin/Eval.h" +#include "builtin/FinalizationRegistryObject.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" @@ -538,6 +539,7 @@ GlobalObject::initSelfHostingBuiltins(JSContext* cx, Handle globa InitBareBuiltinCtor(cx, global, JSProto_Uint8Array) && InitBareBuiltinCtor(cx, global, JSProto_Int32Array) && InitBareSymbolCtor(cx, global) && + InitBareFinalizationRegistryCtor(cx, global) && InitBareWeakMapCtor(cx, global) && InitBareWeakRefCtor(cx, global) && InitStopIterationClass(cx, global) && diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 3ab5f50ccd..99eed23c5f 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -195,6 +195,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime) #endif scriptAndCountsVector(nullptr), weakRefKeptObjects(nullptr), + finalizationRegistryCleanupJobs(nullptr), lcovOutput(), NaNValue(DoubleNaNValue()), negativeInfinityValue(DoubleValue(NegativeInfinity())), @@ -374,6 +375,7 @@ JSRuntime::destroyRuntime() MOZ_ASSERT(childRuntimeCount == 0); clearWeakRefKeptObjects(); + clearFinalizationRegistryCleanupJobs(); fx.destroyInstance(); @@ -518,6 +520,74 @@ JSRuntime::clearWeakRefKeptObjects() weakRefKeptObjects = nullptr; } +bool +JSRuntime::enqueueFinalizationRegistryCleanupJob(JSContext* cx, JS::HandleObject job) +{ + MOZ_ASSERT(cx->runtime() == this); + MOZ_ASSERT(job); + MOZ_ASSERT(job->is()); + MOZ_ASSERT(isHeapBusy()); + + if (!finalizationRegistryCleanupJobs) { + auto* cleanupJobs = + cx->new_>( + cx, js::FinalizationRegistryCleanupJobVector(js::SystemAllocPolicy())); + if (!cleanupJobs) + return false; + + finalizationRegistryCleanupJobs = cleanupJobs; + } + + for (size_t i = 0; i < finalizationRegistryCleanupJobs->length(); i++) { + if ((*finalizationRegistryCleanupJobs)[i] == job) + return true; + } + + if (!finalizationRegistryCleanupJobs->append(job)) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +bool +JSRuntime::drainFinalizationRegistryCleanupJobs(JSContext* cx) +{ + MOZ_ASSERT(cx->runtime() == this); + MOZ_ASSERT(!isHeapBusy()); + + if (!finalizationRegistryCleanupJobs) + return true; + + if (!enqueuePromiseJobCallback) { + finalizationRegistryCleanupJobs->clear(); + return true; + } + + size_t length = finalizationRegistryCleanupJobs->length(); + for (size_t i = 0; i < length; i++) { + RootedFunction job(cx, &(*finalizationRegistryCleanupJobs)[i]->as()); + if (!enqueuePromiseJob(cx, job, nullptr, nullptr)) + return false; + } + + finalizationRegistryCleanupJobs->clear(); + return true; +} + +void +JSRuntime::clearFinalizationRegistryCleanupJobs() +{ + MOZ_ASSERT(!isHeapBusy()); + + if (!finalizationRegistryCleanupJobs) + return; + + defaultFreeOp()->delete_(finalizationRegistryCleanupJobs); + finalizationRegistryCleanupJobs = nullptr; +} + void JSRuntime::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, JS::RuntimeSizes* rtSizes) { diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index b31b8f2cb3..78d4a5cf2e 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -361,6 +361,7 @@ class PerThreadData using ScriptAndCountsVector = GCVector; using WeakRefKeptObjectVector = JS::GCVector; +using FinalizationRegistryCleanupJobVector = JS::GCVector; class AutoLockForExclusiveAccess; } // namespace js @@ -894,6 +895,13 @@ struct JSRuntime : public JS::shadow::Runtime, [[nodiscard]] bool addWeakRefKeptObject(JSContext* cx, JS::HandleValue target); void clearWeakRefKeptObjects(); + /* Cleanup jobs produced by FinalizationRegistry sweeping, enqueued after GC. */ + JS::PersistentRooted* finalizationRegistryCleanupJobs; + + [[nodiscard]] bool enqueueFinalizationRegistryCleanupJob(JSContext* cx, JS::HandleObject job); + [[nodiscard]] bool drainFinalizationRegistryCleanupJobs(JSContext* cx); + void clearFinalizationRegistryCleanupJobs(); + /* Code coverage output. */ js::coverage::LCovRuntime lcovOutput; diff --git a/js/xpconnect/tests/unit/test_finalization_registry.js b/js/xpconnect/tests/unit/test_finalization_registry.js new file mode 100644 index 0000000000..af6f988a99 --- /dev/null +++ b/js/xpconnect/tests/unit/test_finalization_registry.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function assertThrowsTypeError(fn) { + try { + fn(); + } catch (e) { + do_check_true(e instanceof TypeError); + return; + } + do_throw("expected TypeError"); +} + +add_task(function* test_finalization_registry_api_surface() { + do_check_eq(typeof FinalizationRegistry, "function"); + do_check_eq(FinalizationRegistry.name, "FinalizationRegistry"); + do_check_eq(FinalizationRegistry.length, 1); + + assertThrowsTypeError(() => FinalizationRegistry(function() {})); + assertThrowsTypeError(() => new FinalizationRegistry(1)); + + let registry = new FinalizationRegistry(function() {}); + do_check_eq(Object.prototype.toString.call(registry), "[object FinalizationRegistry]"); + do_check_eq(typeof FinalizationRegistry.prototype.register, "function"); + do_check_eq(FinalizationRegistry.prototype.register.length, 2); + do_check_eq(typeof FinalizationRegistry.prototype.unregister, "function"); + do_check_eq(FinalizationRegistry.prototype.unregister.length, 1); + do_check_eq(FinalizationRegistry.prototype.cleanupSome, undefined); + + let desc = Object.getOwnPropertyDescriptor(FinalizationRegistry.prototype, + Symbol.toStringTag); + do_check_eq(desc.value, "FinalizationRegistry"); + do_check_eq(desc.writable, false); + do_check_eq(desc.enumerable, false); + do_check_eq(desc.configurable, true); +}); + +add_task(function* test_register_validation_and_unregister() { + let registry = new FinalizationRegistry(function() {}); + let target = {}; + let token = {}; + + do_check_eq(registry.register(target, "held"), undefined); + do_check_eq(registry.register({}, "held", token), undefined); + do_check_eq(registry.unregister(token), true); + do_check_eq(registry.unregister({}), false); + + assertThrowsTypeError(() => registry.register(1, "held")); + assertThrowsTypeError(() => registry.register(target, target)); + assertThrowsTypeError(() => registry.register(target, "held", 1)); + assertThrowsTypeError(() => registry.register(Symbol.for("registered"), "held")); + assertThrowsTypeError(() => registry.unregister(undefined)); + + do_check_eq(registry.register(Symbol("target"), "held"), undefined); + let symbolToken = Symbol("token"); + do_check_eq(registry.register({}, "held", symbolToken), undefined); + do_check_eq(registry.unregister(symbolToken), true); +}); + +add_task(function* test_cleanup_callback_after_gc() { + let cleaned = []; + let registry = new FinalizationRegistry(value => cleaned.push(value)); + let token = {}; + + (function() { + let target = {}; + registry.register(target, "held", token); + })(); + + for (let i = 0; i < 4 && cleaned.length == 0; i++) { + Components.utils.forceGC(); + yield Promise.resolve(); + } + + do_check_eq(cleaned.length, 1); + do_check_eq(cleaned[0], "held"); + do_check_eq(registry.unregister(token), false); +}); + +add_task(function* test_unregister_prevents_cleanup() { + let cleaned = []; + let registry = new FinalizationRegistry(value => cleaned.push(value)); + let token = {}; + + (function() { + let target = {}; + registry.register(target, "held", token); + })(); + + do_check_eq(registry.unregister(token), true); + Components.utils.forceGC(); + yield Promise.resolve(); + do_check_eq(cleaned.length, 0); +}); diff --git a/js/xpconnect/tests/unit/xpcshell.ini b/js/xpconnect/tests/unit/xpcshell.ini index b13d8d455f..2a1fb6760d 100644 --- a/js/xpconnect/tests/unit/xpcshell.ini +++ b/js/xpconnect/tests/unit/xpcshell.ini @@ -71,6 +71,7 @@ support-files = [test_import_fail.js] [test_interposition.js] [test_isModuleLoaded.js] +[test_finalization_registry.js] [test_js_weak_references.js] [test_onGarbageCollection-01.js] head = head_ongc.js From 890fb3f3999d5db30b16a197beff949250cdb8f2 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 19:08:17 -0400 Subject: [PATCH 18/19] Fix FinalizationRegistry constructor realm prototype --- js/src/builtin/FinalizationRegistryObject.cpp | 26 ++++++++++++++++++- .../tests/unit/test_finalization_registry.js | 13 ++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/js/src/builtin/FinalizationRegistryObject.cpp b/js/src/builtin/FinalizationRegistryObject.cpp index e2a67efd42..60d62eb404 100644 --- a/js/src/builtin/FinalizationRegistryObject.cpp +++ b/js/src/builtin/FinalizationRegistryObject.cpp @@ -38,6 +38,30 @@ NormalizeWeakObject(JSObject* obj) return obj; } +static bool +GetPrototypeFromFinalizationRegistryConstructor(JSContext* cx, HandleObject newTarget, + MutableHandleObject proto) +{ + if (!GetPrototypeFromConstructor(cx, newTarget, proto)) + return false; + if (proto) + return true; + + RootedObject realmObject(cx, CheckedUnwrap(newTarget, /* stopAtWindowProxy = */ false)); + if (!realmObject) + return false; + + { + JSAutoCompartment ac(cx, realmObject); + Rooted global(cx, &realmObject->global()); + if (!GlobalObject::ensureConstructor(cx, global, JSProto_FinalizationRegistry)) + return false; + proto.set(&global->getPrototype(JSProto_FinalizationRegistry).toObject()); + } + + return cx->compartment()->wrap(cx, proto); +} + struct WeakCell { WeakCellKind kind; WeakRef object; @@ -416,7 +440,7 @@ FinalizationRegistryObject::construct(JSContext* cx, unsigned argc, Value* vp) RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromFinalizationRegistryConstructor(cx, newTarget, &proto)) return false; Rooted obj(cx, diff --git a/js/xpconnect/tests/unit/test_finalization_registry.js b/js/xpconnect/tests/unit/test_finalization_registry.js index af6f988a99..f36d628f31 100644 --- a/js/xpconnect/tests/unit/test_finalization_registry.js +++ b/js/xpconnect/tests/unit/test_finalization_registry.js @@ -58,6 +58,19 @@ add_task(function* test_register_validation_and_unregister() { do_check_eq(registry.unregister(symbolToken), true); }); +add_task(function* test_proto_from_constructor_realm() { + let other = new Components.utils.Sandbox("http://example.com", { freshZone: true }); + Components.utils.evalInSandbox("var newTarget = new Function();", other); + + for (let proto of [undefined, null, true, "", Symbol(), 1]) { + other.newTarget.prototype = proto; + let registry = Reflect.construct(FinalizationRegistry, [function() {}], + other.newTarget); + do_check_true(Object.getPrototypeOf(registry) === + other.FinalizationRegistry.prototype); + } +}); + add_task(function* test_cleanup_callback_after_gc() { let cleaned = []; let registry = new FinalizationRegistry(value => cleaned.push(value)); From f3c6da59876d4dfcb9a57bf6b022de40553cdd37 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 19:17:00 -0400 Subject: [PATCH 19/19] Fix WeakRef constructor realm prototype --- js/src/builtin/WeakRefObject.cpp | 27 +++++++++++- js/xpconnect/tests/unit/test_weakref.js | 58 +++++++++++++++++++++++++ js/xpconnect/tests/unit/xpcshell.ini | 1 + 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 js/xpconnect/tests/unit/test_weakref.js diff --git a/js/src/builtin/WeakRefObject.cpp b/js/src/builtin/WeakRefObject.cpp index ed92f547d7..ee162836c1 100644 --- a/js/src/builtin/WeakRefObject.cpp +++ b/js/src/builtin/WeakRefObject.cpp @@ -13,6 +13,7 @@ #include "vm/GlobalObject.h" #include "vm/Symbol.h" +#include "jswrapper.h" #include "jsobjinlines.h" #include "vm/Interpreter-inl.h" @@ -32,6 +33,30 @@ IsWeakRef(HandleValue v) return v.isObject() && v.toObject().is(); } +static bool +GetPrototypeFromWeakRefConstructor(JSContext* cx, HandleObject newTarget, + MutableHandleObject proto) +{ + if (!GetPrototypeFromConstructor(cx, newTarget, proto)) + return false; + if (proto) + return true; + + RootedObject realmObject(cx, CheckedUnwrap(newTarget, /* stopAtWindowProxy = */ false)); + if (!realmObject) + return false; + + { + JSAutoCompartment ac(cx, realmObject); + Rooted global(cx, &realmObject->global()); + if (!GlobalObject::ensureConstructor(cx, global, JSProto_WeakRef)) + return false; + proto.set(&global->getPrototype(JSProto_WeakRef).toObject()); + } + + return cx->compartment()->wrap(cx, proto); +} + static bool WeakRef_deref_impl(JSContext* cx, const CallArgs& args) { @@ -155,7 +180,7 @@ WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromWeakRefConstructor(cx, newTarget, &proto)) return false; Rooted obj(cx, WeakRefObject::create(cx, target, proto)); diff --git a/js/xpconnect/tests/unit/test_weakref.js b/js/xpconnect/tests/unit/test_weakref.js new file mode 100644 index 0000000000..6533a3d425 --- /dev/null +++ b/js/xpconnect/tests/unit/test_weakref.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function assertThrowsTypeError(fn) { + try { + fn(); + } catch (e) { + do_check_true(e instanceof TypeError); + return; + } + do_throw("expected TypeError"); +} + +add_task(function* test_weakref_api_surface() { + do_check_eq(typeof WeakRef, "function"); + do_check_eq(WeakRef.name, "WeakRef"); + do_check_eq(WeakRef.length, 1); + + assertThrowsTypeError(() => WeakRef({})); + assertThrowsTypeError(() => new WeakRef(1)); + assertThrowsTypeError(() => new WeakRef(Symbol.for("registered"))); + + let target = {}; + let ref = new WeakRef(target); + do_check_eq(Object.prototype.toString.call(ref), "[object WeakRef]"); + do_check_eq(ref.deref(), target); + do_check_eq(typeof WeakRef.prototype.deref, "function"); + do_check_eq(WeakRef.prototype.deref.length, 0); + + let desc = Object.getOwnPropertyDescriptor(WeakRef.prototype, + Symbol.toStringTag); + do_check_eq(desc.value, "WeakRef"); + do_check_eq(desc.writable, false); + do_check_eq(desc.enumerable, false); + do_check_eq(desc.configurable, true); +}); + +add_task(function* test_newtarget_prototype_is_not_object() { + function newTarget() {} + + for (let proto of [undefined, null, true, "", Symbol(), 1]) { + newTarget.prototype = proto; + let ref = Reflect.construct(WeakRef, [{}], newTarget); + do_check_true(Object.getPrototypeOf(ref) === WeakRef.prototype); + } +}); + +add_task(function* test_proto_from_constructor_realm() { + let other = new Components.utils.Sandbox("http://example.com", { freshZone: true }); + Components.utils.evalInSandbox("var newTarget = new Function();", other); + + for (let proto of [undefined, null, true, "", Symbol(), 1]) { + other.newTarget.prototype = proto; + let ref = Reflect.construct(WeakRef, [{}], other.newTarget); + do_check_true(Object.getPrototypeOf(ref) === other.WeakRef.prototype); + } +}); diff --git a/js/xpconnect/tests/unit/xpcshell.ini b/js/xpconnect/tests/unit/xpcshell.ini index 2a1fb6760d..f51403cdc2 100644 --- a/js/xpconnect/tests/unit/xpcshell.ini +++ b/js/xpconnect/tests/unit/xpcshell.ini @@ -73,6 +73,7 @@ support-files = [test_isModuleLoaded.js] [test_finalization_registry.js] [test_js_weak_references.js] +[test_weakref.js] [test_onGarbageCollection-01.js] head = head_ongc.js [test_onGarbageCollection-02.js]