Bug 1746515 - Implement Array Buffer Clone r=sfink

Add tests for ArrayBufferClone, as well as ArrayBufferCopyData.

Adapt ArrayBufferCopyData to handle multiple compartments, and do runtime error checking.

Differential Revision: https://phabricator.services.mozilla.com/D134080
This commit is contained in:
Matthew Gaudet 2022-01-04 17:58:47 +00:00
parent 7eb72ab403
commit df1c982518
4 changed files with 240 additions and 17 deletions

View File

@ -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<JSObject*> toBlock,
size_t toIndex,
Handle<JSObject*> fromBlock,
size_t fromIndex, size_t count);
[[nodiscard]] extern JS_PUBLIC_API bool ArrayBufferCopyData(
JSContext* cx, Handle<JSObject*> toBlock, size_t toIndex,
Handle<JSObject*> 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<JSObject*> srcBuffer,
size_t srcByteOffset,
size_t srcLength);
} // namespace JS

View File

@ -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")

View File

@ -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<unsigned int>(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<unsigned int>(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)

View File

@ -2044,25 +2044,84 @@ JS::ArrayBuffer JS::ArrayBuffer::unwrap(JSObject* maybeWrapped) {
return fromObject(ab);
}
void JS::ArrayBufferCopyData(JSContext* cx, Handle<JSObject*> toBlock,
bool JS::ArrayBufferCopyData(JSContext* cx, Handle<JSObject*> toBlock,
size_t toIndex, Handle<JSObject*> fromBlock,
size_t fromIndex, size_t count) {
MOZ_ASSERT(toBlock->is<ArrayBufferObjectMaybeShared>());
MOZ_ASSERT(fromBlock->is<ArrayBufferObjectMaybeShared>());
Rooted<ArrayBufferObjectMaybeShared*> unwrappedToBlock(
cx, toBlock->maybeUnwrapIf<ArrayBufferObjectMaybeShared>());
if (!unwrappedToBlock) {
ReportAccessDenied(cx);
return false;
}
// If both are array bufferrs, can use ArrayBufferCopyData
if (toBlock->is<ArrayBufferObject>() && fromBlock->is<ArrayBufferObject>()) {
Rooted<ArrayBufferObject*> toArray(cx, &toBlock->as<ArrayBufferObject>());
Rooted<ArrayBufferObject*> fromArray(cx,
&fromBlock->as<ArrayBufferObject>());
Rooted<ArrayBufferObjectMaybeShared*> unwrappedFromBlock(
cx, fromBlock->maybeUnwrapIf<ArrayBufferObjectMaybeShared>());
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<ArrayBufferObject>() &&
unwrappedFromBlock->is<ArrayBufferObject>()) {
Rooted<ArrayBufferObject*> toArray(
cx, &unwrappedToBlock->as<ArrayBufferObject>());
Rooted<ArrayBufferObject*> fromArray(
cx, &unwrappedFromBlock->as<ArrayBufferObject>());
ArrayBufferObject::copyData(toArray, toIndex, fromArray, fromIndex, count);
return;
return true;
}
Rooted<ArrayBufferObjectMaybeShared*> toArray(
cx, &toBlock->as<ArrayBufferObjectMaybeShared>());
cx, &unwrappedToBlock->as<ArrayBufferObjectMaybeShared>());
Rooted<ArrayBufferObjectMaybeShared*> fromArray(
cx, &toBlock->as<ArrayBufferObjectMaybeShared>());
cx, &unwrappedFromBlock->as<ArrayBufferObjectMaybeShared>());
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<JSObject*> srcBuffer,
size_t srcByteOffset, size_t srcLength) {
MOZ_ASSERT(srcBuffer->is<ArrayBufferObjectMaybeShared>());
// 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;
}