diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp index a4ae0736d4..f917db9b21 100644 --- a/js/src/builtin/AtomicsObject.cpp +++ b/js/src/builtin/AtomicsObject.cpp @@ -47,10 +47,12 @@ #include "builtin/AtomicsObject.h" #include "mozilla/Atomics.h" +#include "mozilla/Casting.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" #include "mozilla/Unused.h" +#include "builtin/Promise.h" #include "jsapi.h" #include "jsfriendapi.h" #include "jsnum.h" @@ -58,7 +60,9 @@ #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" #include "vm/TypedArrayObject.h" #include "wasm/WasmInstance.h" @@ -121,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) @@ -191,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; @@ -259,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); } @@ -337,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; @@ -380,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; @@ -434,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 @@ -448,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 @@ -462,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 @@ -476,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 @@ -490,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 @@ -693,11 +831,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 +851,34 @@ class FutexWaiter public: FutexWaiter(uint32_t offset, JSRuntime* rt) : offset(offset), + kind(Sync), rt(rt), + asyncTask(nullptr), lower_pri(nullptr), back(nullptr) { } - uint32_t offset; // int32 element index within the SharedArrayBuffer + 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; // byte offset 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 +902,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) { @@ -759,26 +1106,24 @@ 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; - mozilla::Maybe timeout; - if (!timeoutv.isUndefined()) { - double timeout_ms; - if (!ToNumber(cx, timeoutv, &timeout_ms)) + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) 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)); - } + } 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; if (!rt->fx.canWait()) return ReportCannotWait(cx); @@ -787,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; } @@ -796,16 +1140,8 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) Rooted sab(cx, view->bufferShared()); 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); - } + FutexWaiter w(GetWaiterByteOffset(view, offset), rt); + AddWaiter(sarb, &w); FutexRuntime::WaitResult result = FutexRuntime::FutexOK; bool retval = rt->fx.wait(cx, lock.unique(), timeout, &result); @@ -820,15 +1156,96 @@ js::atomics_wait(JSContext* cx, unsigned argc, Value* vp) } } - if (w.lower_pri == &w) { - sarb->setWaiters(nullptr); + 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 (!IsWaitableTypedArray(view->type())) + return ReportBadArrayType(cx); + uint32_t offset; + if (!GetTypedArrayIndex(cx, idxv, view, &offset)) + return false; + uint64_t value = 0; + if (view->type() == Scalar::BigInt64) { + if (!BigIntToRawBits(cx, view->type(), valv, &value)) + return false; } else { - w.lower_pri->back = w.back; - w.back->lower_pri = w.lower_pri; - if (sarb->waiters() == &w) - sarb->setWaiters(w.lower_pri); + int32_t int32Value; + if (!ToInt32(cx, valv, &int32Value)) + return false; + value = uint32_t(int32Value); } - return retval; + 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; + + if (!WaitValueMatches(view, offset, 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; + } + + task->waiter()->offset = GetWaiterByteOffset(view, offset); + 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 @@ -843,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(); @@ -870,9 +1288,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 +1524,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 6e519cfa4a..a5c4793ef0 100644 --- a/js/src/builtin/AtomicsObject.h +++ b/js/src/builtin/AtomicsObject.h @@ -35,6 +35,7 @@ class AtomicsObject : public JSObject [[nodiscard]] bool atomics_xor(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] bool atomics_isLockFree(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] bool atomics_wait(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] bool atomics_waitAsync(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] bool atomics_notify(JSContext* cx, unsigned argc, Value* vp); /* asm.js callouts */ 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 76f85cad63..38445f710b 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 c62bdd633a..8e0336eab7 100644 --- a/js/src/builtin/MapObject.h +++ b/js/src/builtin/MapObject.h @@ -110,7 +110,9 @@ class MapObject : public NativeObject { [[nodiscard]] static bool getKeysAndValuesInterleaved(JSContext* cx, HandleObject obj, JS::MutableHandle> entries); [[nodiscard]] static bool entries(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static 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 { [[nodiscard]] static bool size_impl(JSContext* cx, const CallArgs& args); [[nodiscard]] static bool size(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] static bool get_impl(JSContext* cx, const CallArgs& args); - [[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] static bool has_impl(JSContext* cx, const CallArgs& args); [[nodiscard]] static bool set_impl(JSContext* cx, const CallArgs& args); - [[nodiscard]] static bool set(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] static bool delete_impl(JSContext* cx, const CallArgs& args); [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); [[nodiscard]] static 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 68f479a301..73f2feffe7 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/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 16603d9b86..93214355f2 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; } } @@ -1130,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); } @@ -1472,6 +1491,8 @@ function TypedArraySubarray(begin, end) { "TypedArraySubarray"); } + GetAttachedArrayBuffer(obj); + // Steps 4-6. var buffer = TypedArrayBuffer(obj); var srcLength = TypedArrayLength(obj); @@ -1952,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/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/jit/BaselineIC.cpp b/js/src/jit/BaselineIC.cpp index 5b13772254..da7eb80ac0 100644 --- a/js/src/jit/BaselineIC.cpp +++ b/js/src/jit/BaselineIC.cpp @@ -1470,6 +1470,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())) { @@ -2160,6 +2166,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) { @@ -2631,6 +2639,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 @@ -3221,6 +3232,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 ff8d786e1c..b834b79468 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 af06eea10a..218edc45ad 100644 --- a/js/src/jit/CodeGenerator.h +++ b/js/src/jit/CodeGenerator.h @@ -537,8 +537,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 f4a1c0c199..a82b423ea7 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; - - if (!IsTypedArrayObject(constraints(), callInfo.getArg(0))) - return InliningStatus_NotInlined; + (void) callInfo; - 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; - - if (callInfo.getArg(0)->type() != MIRType::Object) - return false; - - if (callInfo.getArg(1)->type() != MIRType::Int32) - return false; + (void) callInfo; + (void) arrayType; + (void) requiresTagCheck; + (void) checkResult; - // 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 339ef9907b..b86358ac43 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; - - 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; + (void) constraints; + (void) obj; + (void) id; + (void) arrayType; + + // 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 2805651f12..edf39837cd 100644 --- a/js/src/jit/SharedIC.cpp +++ b/js/src/jit/SharedIC.cpp @@ -3631,6 +3631,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 f2da0b95cf..411e87991a 100644 --- a/js/src/jit/SharedIC.h +++ b/js/src/jit/SharedIC.h @@ -2307,6 +2307,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); + [[nodiscard]] bool DoCallNativeGetter(JSContext* cx, HandleFunction callee, HandleObject obj, MutableHandleValue result); diff --git a/js/src/js.msg b/js/src/js.msg index b1cbe57a79..967d075449 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -552,6 +552,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") @@ -559,12 +561,15 @@ 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") MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray of at least length {0}, but constructor returned TypedArray of length {1}") // 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/jsfriendapi.h b/js/src/jsfriendapi.h index 508ddc4419..e1f15a39ae 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/jsstr.cpp b/js/src/jsstr.cpp index f56fae9dd3..42f10ca937 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/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/shell/js.cpp b/js/src/shell/js.cpp index 0f2da0b038..3ff5606fa4 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -3242,6 +3242,15 @@ static const JSClass sandbox_class = { &sandbox_classOps }; +enum GlobalAppSlot { + GlobalAppSlotModuleMetadataHook, + GlobalAppSlotModuleDynamicImportHook, + GlobalAppSlotCount +}; + +static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, + "global application slots overflow"); + static void SetStandardCompartmentOptions(JS::CompartmentOptions& options) { @@ -4070,7 +4079,7 @@ ParseModule(JSContext* cx, unsigned argc, Value* vp) const char16_t* chars = stableChars.twoByteRange().begin().get(); JS::SourceBufferHolder srcBuf(chars, scriptContents->length(), - SourceBufferHolder::NoOwnership); + JS::SourceBufferHolder::NoOwnership); RootedObject module(cx, frontend::CompileModule(cx, options, srcBuf)); if (!module) @@ -4281,7 +4290,7 @@ AbortDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp) RootedString specifier(cx, args[1].toString()); Rooted promise(cx, &args[2].toObject().as()); - cx->setPendingException(args[3]); + cx->setPendingException(args[3], nullptr); return js::FinishDynamicModuleImport(cx, args[0], specifier, promise); } @@ -8290,7 +8299,7 @@ main(int argc, char** argv, char** envp) JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook); JS::SetModuleDynamicImportHook(cx, ShellModuleDynamicImportHook); - JS::SetModuleMetadataHook(cx, ShellModuleMetadataHook); + JS::SetModuleMetadataHook(cx, CallModuleMetadataHook); result = Shell(cx, &op, envp); 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..f603edabff --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-transfer.js @@ -0,0 +1,98 @@ +// |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); +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); +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); + +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") + reportCompare(0, 0); 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..e8d810b165 --- /dev/null +++ b/js/src/tests/non262/ArrayBuffer/resizable-views.js @@ -0,0 +1,218 @@ +// |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); +assertThrowsInstanceOf(() => dv.byteOffset, TypeError); +assertThrowsInstanceOf(() => dv.byteLength, TypeError); +assertThrowsInstanceOf(() => dv.getUint8(0), TypeError); + +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); +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 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 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) { + 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); +gsab.grow(8); +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; +} + +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/tests/non262/Atomics/waitAsync.js b/js/src/tests/non262/Atomics/waitAsync.js new file mode 100644 index 0000000000..a2b23a4f80 --- /dev/null +++ b/js/src/tests/non262/Atomics/waitAsync.js @@ -0,0 +1,77 @@ +// |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 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") + reportCompare(true, true); 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/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/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/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/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/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 511db8fdaf..5894a4cbcf 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,66 @@ 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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_NOT_RESIZABLE); + return false; +} + +static bool +ReportArrayBufferCannotDetach(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ARRAYBUFFER_CANNOT_DETACH); + 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.byteOffsetMaybeOutOfBounds(); + if (byteOffset > newByteLength) + return false; + uint32_t byteLength = dataView.isLengthTracking() + ? newByteLength - byteOffset + : dataView.fixedByteLengthMaybeOutOfBounds(); + return byteOffset <= newByteLength && byteLength <= newByteLength - byteOffset; + } + + if (view->is()) { + TypedArrayObject& typedArray = view->as(); + if (typedArray.isSharedMemory()) + return true; + 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; + } + + // Outline typed objects don't have a recoverable fixed byte range here. + return false; +} + static void NoteViewBufferWasDetached(ArrayBufferViewObject* view, ArrayBufferObject::BufferContents newContents, @@ -391,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()); @@ -402,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); } @@ -428,10 +590,180 @@ 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 +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); + 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 (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 (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); + } +} + +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); } /* @@ -887,6 +1219,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) { @@ -929,7 +1269,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 @@ -1035,9 +1375,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. @@ -1046,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; @@ -1066,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 - 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)); @@ -1098,9 +1429,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; @@ -1130,25 +1462,28 @@ 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 * 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(); - Rooted buffer(cx, &args.thisv().toObject().as()); + 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); @@ -1159,7 +1494,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 @@ -1528,6 +1863,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); } } @@ -1578,6 +1915,8 @@ ArrayBufferViewObject::dataPointerUnshared(const JS::AutoRequireNoGC& nogc) bool ArrayBufferViewObject::isSharedMemory() { + if (is()) + return as().isSharedMemory(); if (is()) return as().isSharedMemory(); return false; @@ -1609,7 +1948,7 @@ ArrayBufferViewObject::bufferObject(JSContext* cx, Handleas().bufferEither(); } MOZ_ASSERT(thisObject->is()); - return &thisObject->as().arrayBuffer(); + return &thisObject->as().arrayBufferEither(); } /* JS Friend API */ @@ -1860,7 +2199,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(); @@ -1930,7 +2269,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 31aae59d06..c38dc2d7c6 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); } @@ -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.) @@ -302,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; @@ -311,6 +333,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 +357,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 +415,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 +433,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") \ 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 56c9ba7f09..70b2923d74 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/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index c9f290463e..7b61a7ddd1 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) { @@ -270,6 +279,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) { @@ -1287,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) { @@ -1409,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(); @@ -2255,7 +2312,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), @@ -2303,6 +2363,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), @@ -2462,6 +2523,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), 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(); } 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/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"); + + 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() || source->as().isSharedMemory(); + 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 fd182c97b3..5fafb5d6fb 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); @@ -508,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); @@ -547,7 +555,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 +906,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 +915,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 +939,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 @@ -1318,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(); @@ -1857,7 +1878,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); @@ -1866,8 +1888,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); - MOZ_ASSERT(!arrayBuffer || !arrayBuffer->is()); + + 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); @@ -1891,57 +1923,72 @@ 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()); 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); + 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; } 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)) { + 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 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; } @@ -1955,14 +2002,18 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons if (args.get(2).isUndefined()) { byteLength -= byteOffset; + lengthTracking = isResizableOrGrowable; } else { - if (!ToUint32(cx, args[2], &byteLength)) + uint64_t viewByteLength; + if (!ToIndex(cx, args[2], &viewByteLength)) return false; - if (byteLength > INT32_MAX) { + lengthTracking = false; + 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"); @@ -1981,6 +2032,7 @@ DataViewObject::getAndCheckConstructorArgs(JSContext* cx, JSObject* bufobj, cons *byteOffsetPtr = byteOffset; *byteLengthPtr = byteLength; + *lengthTrackingPtr = lengthTracking; return true; } @@ -1992,7 +2044,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); @@ -2000,8 +2053,9 @@ DataViewObject::constructSameCompartment(JSContext* cx, HandleObject bufobj, con if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - Rooted buffer(cx, &AsArrayBuffer(bufobj)); - JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto); + Rooted buffer(cx, &bufobj->as()); + JSObject* obj = DataViewObject::create(cx, byteOffset, byteLength, buffer, proto, + lengthTracking); if (!obj) return false; args.rval().setObject(*obj); @@ -2041,8 +2095,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 +2116,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)); @@ -2090,6 +2149,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, @@ -2194,7 +2258,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; } @@ -2293,7 +2357,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; } @@ -3118,7 +3182,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; } diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h index 8ff820b916..f5d9176be3 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,21 +551,66 @@ 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 arrayBufferEither().byteLength() - byteOffsetMaybeOutOfBounds(); + return fixedByteLengthMaybeOutOfBounds(); + } + + ArrayBufferObjectMaybeShared& arrayBufferEither() const { + return bufferValue(const_cast(this)). + toObject().as(); } - ArrayBufferObject& arrayBuffer() const { - return bufferValue(const_cast(this)).toObject().as(); + bool isSharedMemory() const { + return bufferValue(const_cast(this)). + toObject().is(); } void* dataPointer() const { 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 ArrayBufferObjectMaybeShared& buffer = arrayBufferEither(); + 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);