diff --git a/js/public/ArrayBuffer.h b/js/public/ArrayBuffer.h index e6a53859a5bb..32f0c40824a0 100644 --- a/js/public/ArrayBuffer.h +++ b/js/public/ArrayBuffer.h @@ -268,16 +268,32 @@ extern JS_PUBLIC_API void SetLargeArrayBuffersEnabled(bool enable); /** * Copy data from one array buffer to another. * - * Both fromBuffer and toBuffer must be ArrayBufferObjectMaybeShared. + * Both fromBuffer and toBuffer must be (possibly wrapped) + * ArrayBufferObjectMaybeShared. + * + * This method may throw if the sizes don't match, or if unwrapping fails. * * The API for this is modelled on CopyDataBlockBytes from the spec: * https://tc39.es/ecma262/#sec-copydatablockbytes */ -extern JS_PUBLIC_API void ArrayBufferCopyData(JSContext* cx, - Handle toBlock, - size_t toIndex, - Handle fromBlock, - size_t fromIndex, size_t count); +[[nodiscard]] extern JS_PUBLIC_API bool ArrayBufferCopyData( + JSContext* cx, Handle toBlock, size_t toIndex, + Handle fromBlock, size_t fromIndex, size_t count); + +/** + * Copy data from one array buffer to another. + * + * srcBuffer must be a (possibly wrapped) ArrayBufferObjectMaybeShared. + * + * This method may throw if unwrapping or allocation fails. + * + * The API for this is modelled on CloneArrayBuffer from the spec: + * https://tc39.es/ecma262/#sec-clonearraybuffer + */ +extern JS_PUBLIC_API JSObject* ArrayBufferClone(JSContext* cx, + Handle srcBuffer, + size_t srcByteOffset, + size_t srcLength); } // namespace JS diff --git a/js/public/friend/ErrorNumbers.msg b/js/public/friend/ErrorNumbers.msg index 8096d2e26a93..12dd788227c5 100644 --- a/js/public/friend/ErrorNumbers.msg +++ b/js/public/friend/ErrorNumbers.msg @@ -674,6 +674,8 @@ MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray MSG_DEF(JSMSG_TYPED_ARRAY_NOT_COMPATIBLE, 2, JSEXN_TYPEERR, "{0} elements are incompatible with {1}") MSG_DEF(JSMSG_ARRAYBUFFER_REQUIRED, 0, JSEXN_TYPEERR, "ArrayBuffer object required") +MSG_DEF(JSMSG_ARRAYBUFFER_COPY_RANGE, 0, JSEXN_RANGEERR, "ArrayBuffer range incorrect for copying") + // Shared array buffer MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range") MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer") diff --git a/js/src/jsapi-tests/testArrayBuffer.cpp b/js/src/jsapi-tests/testArrayBuffer.cpp index 7fa9bfcecbbb..fd74993893f3 100644 --- a/js/src/jsapi-tests/testArrayBuffer.cpp +++ b/js/src/jsapi-tests/testArrayBuffer.cpp @@ -5,14 +5,18 @@ #include "builtin/TestingFunctions.h" #include "js/Array.h" // JS::NewArrayObject #include "js/ArrayBuffer.h" // JS::{GetArrayBuffer{ByteLength,Data},IsArrayBufferObject,NewArrayBuffer{,WithContents},StealArrayBufferContents} +#include "js/ArrayBufferMaybeShared.h" #include "js/CallAndConstruct.h" #include "js/Exception.h" #include "js/experimental/TypedData.h" // JS_New{Int32,Uint8}ArrayWithBuffer #include "js/friend/ErrorMessages.h" // JSMSG_* #include "js/MemoryFunctions.h" #include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty, JS_SetElement +#include "js/Realm.h" #include "jsapi-tests/tests.h" +#include "vm/Realm-inl.h" + BEGIN_TEST(testArrayBuffer_bug720949_steal) { static const unsigned NUM_TEST_BUFFERS = 2; static const unsigned MAGIC_VALUE_1 = 3; @@ -301,3 +305,145 @@ BEGIN_TEST(testArrayBuffer_serializeExternal) { return true; } END_TEST(testArrayBuffer_serializeExternal) + +BEGIN_TEST(testArrayBuffer_copyData) { + ExternalData data1("One two three four"); + JS::RootedObject buffer1(cx, JS::NewExternalArrayBuffer( + cx, data1.len(), data1.contents(), nullptr)); + + CHECK(buffer1); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewExternalArrayBuffer( + cx, data2.len(), data2.contents(), nullptr)); + + CHECK(buffer2); + + // Check we can't copy from a larger to a smaller buffer. + CHECK(!JS::ArrayBufferCopyData(cx, buffer2, 0, buffer1, 0, data1.len())); + + // Verify expected exception is thrown. + { + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + + CHECK_EQUAL(report.report()->errorNumber, + static_cast(JSMSG_ARRAYBUFFER_COPY_RANGE)); + } + + CHECK(JS::ArrayBufferCopyData( + cx, buffer1, 0, buffer2, 0, + data2.len() - 1 /* don't copy null terminator */)); + + { + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(buffer1, &len, &isShared, &bufferData); + + ExternalData expected1("Six two three four"); + + fprintf(stderr, "expected %s actual %s\n", expected1.asString(), + bufferData); + + CHECK_EQUAL(len, expected1.len()); + CHECK_EQUAL(memcmp(expected1.contents(), bufferData, expected1.len()), 0); + } + + return true; +} +END_TEST(testArrayBuffer_copyData) + +BEGIN_TEST(testArrayBuffer_copyDataAcrossGlobals) { + JS::RootedObject otherGlobal(cx, createGlobal(nullptr)); + if (!otherGlobal) { + return false; + } + + ExternalData data1("One two three four"); + JS::RootedObject buffer1(cx); + { + js::AutoRealm realm(cx, otherGlobal); + buffer1 = + JS::NewExternalArrayBuffer(cx, data1.len(), data1.contents(), nullptr); + } + CHECK(buffer1); + CHECK(JS_WrapObject(cx, &buffer1)); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewExternalArrayBuffer( + cx, data2.len(), data2.contents(), nullptr)); + + CHECK(buffer2); + + // Check we can't copy from a larger to a smaller buffer. + CHECK(!JS::ArrayBufferCopyData(cx, buffer2, 0, buffer1, 0, data1.len())); + + // Verify expected exception is thrown. + { + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + + CHECK_EQUAL(report.report()->errorNumber, + static_cast(JSMSG_ARRAYBUFFER_COPY_RANGE)); + } + + CHECK(JS::ArrayBufferCopyData( + cx, buffer1, 0, buffer2, 0, + data2.len() - 1 /* don't copy null terminator */)); + + { + JS::RootedObject unwrappedBuffer1( + cx, JS::UnwrapArrayBufferMaybeShared(buffer1)); + CHECK(unwrappedBuffer1); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(unwrappedBuffer1, &len, &isShared, + &bufferData); + + ExternalData expected1("Six two three four"); + + fprintf(stderr, "expected %s actual %s\n", expected1.asString(), + bufferData); + + CHECK_EQUAL(len, expected1.len()); + CHECK_EQUAL(memcmp(expected1.contents(), bufferData, expected1.len()), 0); + } + + return true; +} +END_TEST(testArrayBuffer_copyDataAcrossGlobals) + +BEGIN_TEST(testArrayBuffer_ArrayBufferClone) { + ExternalData data("One two three four"); + JS::RootedObject externalBuffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), nullptr)); + + CHECK(externalBuffer); + + size_t lengthToCopy = 3; + JS::RootedObject clonedBuffer( + cx, JS::ArrayBufferClone(cx, externalBuffer, 4, lengthToCopy)); + CHECK(clonedBuffer); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(clonedBuffer, &len, &isShared, &bufferData); + + CHECK_EQUAL(len, lengthToCopy); + + ExternalData expectedData("two"); + CHECK_EQUAL(memcmp(expectedData.contents(), bufferData, len), 0); + + return true; +} +END_TEST(testArrayBuffer_ArrayBufferClone) diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index bae0ac8b0435..6a1fa3780d9a 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -2044,25 +2044,84 @@ JS::ArrayBuffer JS::ArrayBuffer::unwrap(JSObject* maybeWrapped) { return fromObject(ab); } -void JS::ArrayBufferCopyData(JSContext* cx, Handle toBlock, +bool JS::ArrayBufferCopyData(JSContext* cx, Handle toBlock, size_t toIndex, Handle fromBlock, size_t fromIndex, size_t count) { - MOZ_ASSERT(toBlock->is()); - MOZ_ASSERT(fromBlock->is()); + Rooted unwrappedToBlock( + cx, toBlock->maybeUnwrapIf()); + if (!unwrappedToBlock) { + ReportAccessDenied(cx); + return false; + } - // If both are array bufferrs, can use ArrayBufferCopyData - if (toBlock->is() && fromBlock->is()) { - Rooted toArray(cx, &toBlock->as()); - Rooted fromArray(cx, - &fromBlock->as()); + Rooted unwrappedFromBlock( + cx, fromBlock->maybeUnwrapIf()); + if (!unwrappedFromBlock) { + ReportAccessDenied(cx); + return false; + } + + // Verify that lengths still make sense and throw otherwise. + if (toIndex + count < toIndex || // size_t overflow + fromIndex + count < fromIndex || // size_t overflow + toIndex + count > unwrappedToBlock->byteLength() || + fromIndex + count > unwrappedFromBlock->byteLength()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ARRAYBUFFER_COPY_RANGE); + return false; + } + + // If both are array buffers, can use ArrayBufferCopyData + if (unwrappedToBlock->is() && + unwrappedFromBlock->is()) { + Rooted toArray( + cx, &unwrappedToBlock->as()); + Rooted fromArray( + cx, &unwrappedFromBlock->as()); ArrayBufferObject::copyData(toArray, toIndex, fromArray, fromIndex, count); - return; + return true; } Rooted toArray( - cx, &toBlock->as()); + cx, &unwrappedToBlock->as()); Rooted fromArray( - cx, &toBlock->as()); + cx, &unwrappedFromBlock->as()); SharedArrayBufferObject::copyData(toArray, toIndex, fromArray, fromIndex, count); + + return true; +} + +// https://tc39.es/ecma262/#sec-clonearraybuffer +// We only support the case where cloneConstructor is %ArrayBuffer%. Note, +// this means that cloning a SharedArrayBuffer will produce an ArrayBuffer +JSObject* JS::ArrayBufferClone(JSContext* cx, Handle srcBuffer, + size_t srcByteOffset, size_t srcLength) { + MOZ_ASSERT(srcBuffer->is()); + + // 2. (reordered) If IsDetachedBuffer(srcBuffer) is true, throw a TypeError + // exception. + if (IsDetachedArrayBufferObject(srcBuffer)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return nullptr; + } + + // 1. Let targetBuffer be ? AllocateArrayBuffer(cloneConstructor, srcLength). + JS::RootedObject targetBuffer(cx, JS::NewArrayBuffer(cx, srcLength)); + if (!targetBuffer) { + return nullptr; + } + + // 3. Let srcBlock be srcBuffer.[[ArrayBufferData]]. + // 4. Let targetBlock be targetBuffer.[[ArrayBufferData]]. + // 5. Perform CopyDataBlockBytes(targetBlock, 0, srcBlock, srcByteOffset, + // srcLength). + if (!ArrayBufferCopyData(cx, targetBuffer, 0, srcBuffer, srcByteOffset, + srcLength)) { + return nullptr; + } + + // 6. Return targetBuffer. + return targetBuffer; }