diff --git a/js/src/js.msg b/js/src/js.msg index 499e7a1623..d9a6c98764 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -561,6 +561,7 @@ MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray // Shared array buffer MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range") +MSG_DEF(JSMSG_SHARED_ARRAY_NOT_GROWABLE, 0, JSEXN_TYPEERR, "SharedArrayBuffer is not growable") MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer") MSG_DEF(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different SharedArrayBuffer, but species constructor returned same SharedArrayBuffer") MSG_DEF(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected SharedArrayBuffer with at least {0} bytes, but species constructor returns SharedArrayBuffer with {1} bytes") diff --git a/js/src/tests/non262/SharedArrayBuffer/growable.js b/js/src/tests/non262/SharedArrayBuffer/growable.js new file mode 100644 index 0000000000..7b9ce85ffe --- /dev/null +++ b/js/src/tests/non262/SharedArrayBuffer/growable.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!this.SharedArrayBuffer) + +if (typeof SharedArrayBuffer === "function") { + const fixed = new SharedArrayBuffer(4); + assertEq(fixed.byteLength, 4); + assertEq(fixed.maxByteLength, 4); + assertEq(fixed.growable, false); + assertThrowsInstanceOf(() => fixed.grow(4), TypeError); + + assertThrowsInstanceOf(() => new SharedArrayBuffer(-1), RangeError); + assertThrowsInstanceOf(() => new SharedArrayBuffer(8, {maxByteLength: 4}), RangeError); + + let optionGetterCalled = false; + const growable = new SharedArrayBuffer(4, { + get maxByteLength() { + optionGetterCalled = true; + return 16; + } + }); + assertEq(optionGetterCalled, true); + assertEq(growable.byteLength, 4); + assertEq(growable.maxByteLength, 16); + assertEq(growable.growable, true); + + const before = new Uint8Array(growable); + before[0] = 37; + assertEq(growable.grow(12), undefined); + assertEq(growable.byteLength, 12); + assertEq(growable.maxByteLength, 16); + + const after = new Uint8Array(growable); + assertEq(after.length, 12); + assertEq(after[0], 37); + + assertThrowsInstanceOf(() => growable.grow(11), RangeError); + assertThrowsInstanceOf(() => growable.grow(17), RangeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 7eab0f0e24..739bd0de33 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -1245,7 +1245,7 @@ js::WasmArrayBufferMaxSize(const ArrayBufferObjectMaybeShared* buf) if (buf->is()) return buf->as().wasmMaxSize(); - return Some(buf->as().byteLength()); + return Some(buf->as().maxByteLength()); } /* static */ bool diff --git a/js/src/vm/SharedArrayObject.cpp b/js/src/vm/SharedArrayObject.cpp index 6a3c6a91c3..a597564695 100644 --- a/js/src/vm/SharedArrayObject.cpp +++ b/js/src/vm/SharedArrayObject.cpp @@ -8,6 +8,7 @@ #include "mozilla/Atomics.h" #include "jsfriendapi.h" +#include "jsnum.h" #include "jsprf.h" #ifdef XP_WIN @@ -104,15 +105,18 @@ SharedArrayAllocSize(uint32_t length) } SharedArrayRawBuffer* -SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) +SharedArrayRawBuffer::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable) { // The value (uint32_t)-1 is used as a signal in various places, // so guard against it on principle. MOZ_ASSERT(length != (uint32_t)-1); + MOZ_ASSERT(maxLength != (uint32_t)-1); + MOZ_ASSERT(maxLength >= length); // Add a page for the header and round to a page boundary. - uint32_t allocSize = SharedArrayAllocSize(length); - if (allocSize <= length) + uint32_t allocationLength = growable ? maxLength : length; + uint32_t allocSize = SharedArrayAllocSize(allocationLength); + if (allocSize <= allocationLength) return nullptr; // Test >= to guard against the case where multiple extant runtimes @@ -127,7 +131,8 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) } } - bool preparedForAsmJS = jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); + bool preparedForAsmJS = + !growable && jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); void* p = nullptr; if (preparedForAsmJS) { @@ -161,8 +166,9 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) uint8_t* buffer = reinterpret_cast(p) + gc::SystemPageSize(); uint8_t* base = buffer - sizeof(SharedArrayRawBuffer); - SharedArrayRawBuffer* rawbuf = new (base) SharedArrayRawBuffer(buffer, length, preparedForAsmJS); - MOZ_ASSERT(rawbuf->length == length); // Deallocation needs this + SharedArrayRawBuffer* rawbuf = + new (base) SharedArrayRawBuffer(buffer, length, maxLength, growable, preparedForAsmJS); + MOZ_ASSERT(rawbuf->allocatedByteLength() == allocationLength); // Deallocation needs this. return rawbuf; } @@ -201,7 +207,7 @@ SharedArrayRawBuffer::dropReference() MOZ_ASSERT(p.asValue() % gc::SystemPageSize() == 0); uint8_t* address = p.unwrap(/*safe - only reference*/); - uint32_t allocSize = SharedArrayAllocSize(this->length); + uint32_t allocSize = SharedArrayAllocSize(this->allocatedByteLength()); if (this->preparedForAsmJS) { uint32_t mappedSize = SharedArrayMappedSize(allocSize); @@ -221,6 +227,23 @@ SharedArrayRawBuffer::dropReference() numLive--; } +bool +SharedArrayRawBuffer::growTo(uint32_t newLength) +{ + MOZ_ASSERT(growable); + MOZ_ASSERT(newLength <= maxLength); + + for (;;) { + uint32_t oldLength = length; + if (newLength < oldLength) + return false; + if (newLength == oldLength) + return true; + if (length.compareExchange(oldLength, newLength)) + return true; + } +} + MOZ_ALWAYS_INLINE bool SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args) @@ -237,6 +260,112 @@ SharedArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* v return CallNonGenericMethod(cx, args); } +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setInt32(args.thisv().toObject().as().maxByteLength()); + return true; +} + +bool +SharedArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::growableGetterImpl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + args.rval().setBoolean(args.thisv().toObject().as().isGrowable()); + return true; +} + +bool +SharedArrayBufferObject::growableGetter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool +ReportSharedArrayBufferNotGrowable(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_NOT_GROWABLE); + return false; +} + +static bool +ReportSharedArrayBufferLengthOutOfRange(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + return false; +} + +static bool +GetSharedArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options, + uint32_t byteLength, uint32_t* maxByteLength, + bool* growable) +{ + *maxByteLength = byteLength; + *growable = false; + + if (!options.isObject()) + return true; + + RootedObject opts(cx, &options.toObject()); + RootedValue maxByteLengthValue(cx); + if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue)) + return false; + + if (maxByteLengthValue.isUndefined()) + return true; + + uint64_t max; + if (!ToIndex(cx, maxByteLengthValue, &max)) + return false; + + if (max > INT32_MAX || max < byteLength) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + *maxByteLength = uint32_t(max); + *growable = true; + return true; +} + +MOZ_ALWAYS_INLINE bool +SharedArrayBufferObject::fun_grow_impl(JSContext* cx, const CallArgs& args) +{ + MOZ_ASSERT(IsSharedArrayBuffer(args.thisv())); + + Rooted buffer(cx, + &args.thisv().toObject().as()); + if (!buffer->isGrowable()) + return ReportSharedArrayBufferNotGrowable(cx); + + uint64_t newByteLength; + if (!ToIndex(cx, args.get(0), &newByteLength)) + return false; + if (newByteLength > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + uint32_t newLength = uint32_t(newByteLength); + if (newLength > buffer->maxByteLength() || !buffer->growTo(newLength)) + return ReportSharedArrayBufferLengthOutOfRange(cx); + + args.rval().setUndefined(); + return true; +} + +bool +SharedArrayBufferObject::fun_grow(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + bool SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) { @@ -245,11 +374,19 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!ThrowIfNotConstructing(cx, args, "SharedArrayBuffer")) return false; + uint64_t length64; + if (!ToIndex(cx, args.get(0), &length64)) + return false; // Bugs 1068458, 1161298: Limit length to 2^31-1. - uint32_t length; - bool overflow_unused; - if (!ToLengthClamped(cx, args.get(0), &length, &overflow_unused) || length > INT32_MAX) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH); + if (length64 > INT32_MAX) + return ReportSharedArrayBufferLengthOutOfRange(cx); + uint32_t length = uint32_t(length64); + + uint32_t maxByteLength; + bool growable; + if (!GetSharedArrayBufferMaxByteLengthOption(cx, args.get(1), length, + &maxByteLength, &growable)) + { return false; } @@ -258,7 +395,7 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; - JSObject* bufobj = New(cx, length, proto); + JSObject* bufobj = New(cx, length, maxByteLength, growable, proto); if (!bufobj) return false; args.rval().setObject(*bufobj); @@ -266,9 +403,10 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* } SharedArrayBufferObject* -SharedArrayBufferObject::New(JSContext* cx, uint32_t length, HandleObject proto) +SharedArrayBufferObject::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable, + HandleObject proto) { - SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length); + SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length, maxLength, growable); if (!buffer) return nullptr; @@ -341,7 +479,7 @@ SharedArrayBufferObject::addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSi // just live with the risk. const SharedArrayBufferObject& buf = obj->as(); info->objectsNonHeapElementsShared += - buf.byteLength() / buf.rawBufferObject()->refcount(); + buf.rawBufferObject()->allocatedByteLength() / buf.rawBufferObject()->refcount(); } /* static */ void @@ -409,12 +547,15 @@ static const JSPropertySpec static_properties[] = { }; static const JSFunctionSpec prototype_functions[] = { + JS_FN("grow", SharedArrayBufferObject::fun_grow, 1, 0), JS_SELF_HOSTED_FN("slice", "SharedArrayBufferSlice", 2, 0), JS_FS_END }; static const JSPropertySpec prototype_properties[] = { JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0), + JS_PSG("growable", SharedArrayBufferObject::growableGetter, 0), + JS_PSG("maxByteLength", SharedArrayBufferObject::maxByteLengthGetter, 0), JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY), JS_PS_END }; diff --git a/js/src/vm/SharedArrayObject.h b/js/src/vm/SharedArrayObject.h index 81fb49f3a8..01872aaff6 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(); }