v8/test/cctest/test-api-array-buffer.cc

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

936 lines
32 KiB
C++
Raw Normal View History

// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "test/cctest/test-api.h"
#include "src/api/api-inl.h"
using ::v8::Array;
using ::v8::Context;
using ::v8::Local;
using ::v8::Value;
namespace {
void CheckDataViewIsDetached(v8::Local<v8::DataView> dv) {
CHECK_EQ(0, static_cast<int>(dv->ByteLength()));
CHECK_EQ(0, static_cast<int>(dv->ByteOffset()));
}
void CheckIsDetached(v8::Local<v8::TypedArray> ta) {
CHECK_EQ(0, static_cast<int>(ta->ByteLength()));
CHECK_EQ(0, static_cast<int>(ta->Length()));
CHECK_EQ(0, static_cast<int>(ta->ByteOffset()));
}
void CheckIsTypedArrayVarDetached(const char* name) {
i::ScopedVector<char> source(1024);
i::SNPrintF(source,
"%s.byteLength == 0 && %s.byteOffset == 0 && %s.length == 0",
name, name, name);
CHECK(CompileRun(source.begin())->IsTrue());
v8::Local<v8::TypedArray> ta =
v8::Local<v8::TypedArray>::Cast(CompileRun(name));
CheckIsDetached(ta);
}
template <typename TypedArray, int kElementSize>
Local<TypedArray> CreateAndCheck(Local<v8::ArrayBuffer> ab, int byteOffset,
int length) {
v8::Local<TypedArray> ta = TypedArray::New(ab, byteOffset, length);
CheckInternalFieldsAreZero<v8::ArrayBufferView>(ta);
CHECK_EQ(byteOffset, static_cast<int>(ta->ByteOffset()));
CHECK_EQ(length, static_cast<int>(ta->Length()));
CHECK_EQ(length * kElementSize, static_cast<int>(ta->ByteLength()));
return ta;
}
std::shared_ptr<v8::BackingStore> Externalize(Local<v8::ArrayBuffer> ab) {
std::shared_ptr<v8::BackingStore> backing_store = ab->GetBackingStore();
// Keep the tests until the deprecated functions are removed.
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
ab->Externalize(backing_store);
CHECK(ab->IsExternal());
#if __clang__
#pragma clang diagnostic pop
#endif
return backing_store;
}
std::shared_ptr<v8::BackingStore> Externalize(Local<v8::SharedArrayBuffer> ab) {
std::shared_ptr<v8::BackingStore> backing_store = ab->GetBackingStore();
// Keep the tests until the deprecated functions are removed.
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
ab->Externalize(backing_store);
CHECK(ab->IsExternal());
#if __clang__
#pragma clang diagnostic pop
#endif
return backing_store;
}
} // namespace
THREADED_TEST(ArrayBuffer_ApiInternalToExternal) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 1024);
CheckInternalFieldsAreZero(ab);
CHECK_EQ(1024, ab->ByteLength());
CcTest::CollectAllGarbage();
std::shared_ptr<v8::BackingStore> backing_store = Externalize(ab);
CHECK_EQ(1024, backing_store->ByteLength());
uint8_t* data = static_cast<uint8_t*>(backing_store->Data());
CHECK_NOT_NULL(data);
CHECK(env->Global()->Set(env.local(), v8_str("ab"), ab).FromJust());
v8::Local<v8::Value> result = CompileRun("ab.byteLength");
CHECK_EQ(1024, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8 = new Uint8Array(ab);"
"u8[0] = 0xFF;"
"u8[1] = 0xAA;"
"u8.length");
CHECK_EQ(1024, result->Int32Value(env.local()).FromJust());
CHECK_EQ(0xFF, data[0]);
CHECK_EQ(0xAA, data[1]);
data[0] = 0xCC;
data[1] = 0x11;
result = CompileRun("u8[0] + u8[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
THREADED_TEST(ArrayBuffer_JSInternalToExternal) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Value> result = CompileRun(
"var ab1 = new ArrayBuffer(2);"
"var u8_a = new Uint8Array(ab1);"
"u8_a[0] = 0xAA;"
"u8_a[1] = 0xFF; u8_a.buffer");
Local<v8::ArrayBuffer> ab1 = Local<v8::ArrayBuffer>::Cast(result);
CheckInternalFieldsAreZero(ab1);
CHECK_EQ(2, ab1->ByteLength());
std::shared_ptr<v8::BackingStore> backing_store = Externalize(ab1);
result = CompileRun("ab1.byteLength");
CHECK_EQ(2, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_a[0]");
CHECK_EQ(0xAA, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_a[1]");
CHECK_EQ(0xFF, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8_b = new Uint8Array(ab1);"
"u8_b[0] = 0xBB;"
"u8_a[0]");
CHECK_EQ(0xBB, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_b[1]");
CHECK_EQ(0xFF, result->Int32Value(env.local()).FromJust());
CHECK_EQ(2, backing_store->ByteLength());
uint8_t* ab1_data = static_cast<uint8_t*>(backing_store->Data());
CHECK_EQ(0xBB, ab1_data[0]);
CHECK_EQ(0xFF, ab1_data[1]);
ab1_data[0] = 0xCC;
ab1_data[1] = 0x11;
result = CompileRun("u8_a[0] + u8_a[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
THREADED_TEST(ArrayBuffer_External) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
i::ScopedVector<uint8_t> my_data(100);
memset(my_data.begin(), 0, 100);
// Keep the tests until the deprecated functions are removed.
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
Local<v8::ArrayBuffer> ab3 =
v8::ArrayBuffer::New(isolate, my_data.begin(), 100);
CheckInternalFieldsAreZero(ab3);
CHECK_EQ(100, ab3->ByteLength());
CHECK(ab3->IsExternal());
#if __clang__
#pragma clang diagnostic pop
#endif
CHECK(env->Global()->Set(env.local(), v8_str("ab3"), ab3).FromJust());
v8::Local<v8::Value> result = CompileRun("ab3.byteLength");
CHECK_EQ(100, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8_b = new Uint8Array(ab3);"
"u8_b[0] = 0xBB;"
"u8_b[1] = 0xCC;"
"u8_b.length");
CHECK_EQ(100, result->Int32Value(env.local()).FromJust());
CHECK_EQ(0xBB, my_data[0]);
CHECK_EQ(0xCC, my_data[1]);
my_data[0] = 0xCC;
my_data[1] = 0x11;
result = CompileRun("u8_b[0] + u8_b[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
THREADED_TEST(ArrayBuffer_DisableDetach) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 100);
CHECK(ab->IsDetachable());
i::Handle<i::JSArrayBuffer> buf = v8::Utils::OpenHandle(*ab);
buf->set_is_detachable(false);
CHECK(!ab->IsDetachable());
}
THREADED_TEST(ArrayBuffer_DetachingApi) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(isolate, 1024);
v8::Local<v8::Uint8Array> u8a =
CreateAndCheck<v8::Uint8Array, 1>(buffer, 1, 1023);
v8::Local<v8::Uint8ClampedArray> u8c =
CreateAndCheck<v8::Uint8ClampedArray, 1>(buffer, 1, 1023);
v8::Local<v8::Int8Array> i8a =
CreateAndCheck<v8::Int8Array, 1>(buffer, 1, 1023);
v8::Local<v8::Uint16Array> u16a =
CreateAndCheck<v8::Uint16Array, 2>(buffer, 2, 511);
v8::Local<v8::Int16Array> i16a =
CreateAndCheck<v8::Int16Array, 2>(buffer, 2, 511);
v8::Local<v8::Uint32Array> u32a =
CreateAndCheck<v8::Uint32Array, 4>(buffer, 4, 255);
v8::Local<v8::Int32Array> i32a =
CreateAndCheck<v8::Int32Array, 4>(buffer, 4, 255);
v8::Local<v8::Float32Array> f32a =
CreateAndCheck<v8::Float32Array, 4>(buffer, 4, 255);
v8::Local<v8::Float64Array> f64a =
CreateAndCheck<v8::Float64Array, 8>(buffer, 8, 127);
v8::Local<v8::DataView> dv = v8::DataView::New(buffer, 1, 1023);
CheckInternalFieldsAreZero<v8::ArrayBufferView>(dv);
CHECK_EQ(1, dv->ByteOffset());
CHECK_EQ(1023, dv->ByteLength());
Externalize(buffer);
buffer->Detach();
CHECK_EQ(0, buffer->ByteLength());
CheckIsDetached(u8a);
CheckIsDetached(u8c);
CheckIsDetached(i8a);
CheckIsDetached(u16a);
CheckIsDetached(i16a);
CheckIsDetached(u32a);
CheckIsDetached(i32a);
CheckIsDetached(f32a);
CheckIsDetached(f64a);
CheckDataViewIsDetached(dv);
}
THREADED_TEST(ArrayBuffer_DetachingScript) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
CompileRun(
"var ab = new ArrayBuffer(1024);"
"var u8a = new Uint8Array(ab, 1, 1023);"
"var u8c = new Uint8ClampedArray(ab, 1, 1023);"
"var i8a = new Int8Array(ab, 1, 1023);"
"var u16a = new Uint16Array(ab, 2, 511);"
"var i16a = new Int16Array(ab, 2, 511);"
"var u32a = new Uint32Array(ab, 4, 255);"
"var i32a = new Int32Array(ab, 4, 255);"
"var f32a = new Float32Array(ab, 4, 255);"
"var f64a = new Float64Array(ab, 8, 127);"
"var dv = new DataView(ab, 1, 1023);");
v8::Local<v8::ArrayBuffer> ab =
Local<v8::ArrayBuffer>::Cast(CompileRun("ab"));
v8::Local<v8::DataView> dv = v8::Local<v8::DataView>::Cast(CompileRun("dv"));
Externalize(ab);
ab->Detach();
CHECK_EQ(0, ab->ByteLength());
CHECK_EQ(0, v8_run_int32value(v8_compile("ab.byteLength")));
CheckIsTypedArrayVarDetached("u8a");
CheckIsTypedArrayVarDetached("u8c");
CheckIsTypedArrayVarDetached("i8a");
CheckIsTypedArrayVarDetached("u16a");
CheckIsTypedArrayVarDetached("i16a");
CheckIsTypedArrayVarDetached("u32a");
CheckIsTypedArrayVarDetached("i32a");
CheckIsTypedArrayVarDetached("f32a");
CheckIsTypedArrayVarDetached("f64a");
CHECK(CompileRun("dv.byteLength == 0 && dv.byteOffset == 0")->IsTrue());
CheckDataViewIsDetached(dv);
}
// TODO(v8:9380) the Contents data structure should be deprecated.
THREADED_TEST(ArrayBuffer_AllocationInformation) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
const size_t ab_size = 1024;
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, ab_size);
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
v8::ArrayBuffer::Contents contents(ab->GetContents());
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
#endif
// Array buffers should have normal allocation mode.
CHECK_EQ(contents.AllocationMode(),
v8::ArrayBuffer::Allocator::AllocationMode::kNormal);
// The allocation must contain the buffer (normally they will be equal, but
// this is not required by the contract).
CHECK_NOT_NULL(contents.AllocationBase());
const uintptr_t alloc =
reinterpret_cast<uintptr_t>(contents.AllocationBase());
const uintptr_t data = reinterpret_cast<uintptr_t>(contents.Data());
CHECK_LE(alloc, data);
CHECK_LE(data + contents.ByteLength(), alloc + contents.AllocationLength());
}
THREADED_TEST(ArrayBuffer_ExternalizeEmpty) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 2);
CheckInternalFieldsAreZero(ab);
CHECK_EQ(2, ab->ByteLength());
// Externalize the buffer (taking ownership of the backing store memory).
std::shared_ptr<v8::BackingStore> backing_store = Externalize(ab);
Local<v8::Uint8Array> u8a = v8::Uint8Array::New(ab, 0, 0);
// Calling Buffer() will materialize the ArrayBuffer (transitioning it from
// on-heap to off-heap if need be). This should not affect whether it is
// marked as is_external or not.
USE(u8a->Buffer());
CHECK(ab->IsExternal());
CHECK_EQ(2, backing_store->ByteLength());
}
THREADED_TEST(SharedArrayBuffer_ApiInternalToExternal) {
i::FLAG_harmony_sharedarraybuffer = true;
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::SharedArrayBuffer> ab = v8::SharedArrayBuffer::New(isolate, 1024);
CheckInternalFieldsAreZero(ab);
CHECK_EQ(1024, ab->ByteLength());
CcTest::CollectAllGarbage();
std::shared_ptr<v8::BackingStore> backing_store = Externalize(ab);
CHECK_EQ(1024, backing_store->ByteLength());
uint8_t* data = static_cast<uint8_t*>(backing_store->Data());
CHECK_NOT_NULL(data);
CHECK(env->Global()->Set(env.local(), v8_str("ab"), ab).FromJust());
v8::Local<v8::Value> result = CompileRun("ab.byteLength");
CHECK_EQ(1024, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8 = new Uint8Array(ab);"
"u8[0] = 0xFF;"
"u8[1] = 0xAA;"
"u8.length");
CHECK_EQ(1024, result->Int32Value(env.local()).FromJust());
CHECK_EQ(0xFF, data[0]);
CHECK_EQ(0xAA, data[1]);
data[0] = 0xCC;
data[1] = 0x11;
result = CompileRun("u8[0] + u8[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
THREADED_TEST(ArrayBuffer_ExternalReused) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
i::ScopedVector<uint8_t> data(100);
Local<v8::ArrayBuffer> ab1 = v8::ArrayBuffer::New(isolate, data.begin(), 100);
std::shared_ptr<v8::BackingStore> bs1 = ab1->GetBackingStore();
ab1->Detach();
Local<v8::ArrayBuffer> ab2 = v8::ArrayBuffer::New(isolate, data.begin(), 100);
std::shared_ptr<v8::BackingStore> bs2 = ab2->GetBackingStore();
CHECK_EQ(bs1->Data(), bs2->Data());
}
THREADED_TEST(SharedArrayBuffer_ExternalReused) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
i::ScopedVector<uint8_t> data(100);
Local<v8::SharedArrayBuffer> ab1 =
v8::SharedArrayBuffer::New(isolate, data.begin(), 100);
std::shared_ptr<v8::BackingStore> bs1 = ab1->GetBackingStore();
Local<v8::SharedArrayBuffer> ab2 =
v8::SharedArrayBuffer::New(isolate, data.begin(), 100);
std::shared_ptr<v8::BackingStore> bs2 = ab2->GetBackingStore();
CHECK_EQ(bs1->Data(), bs2->Data());
}
THREADED_TEST(SharedArrayBuffer_JSInternalToExternal) {
i::FLAG_harmony_sharedarraybuffer = true;
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Value> result = CompileRun(
"var ab1 = new SharedArrayBuffer(2);"
"var u8_a = new Uint8Array(ab1);"
"u8_a[0] = 0xAA;"
"u8_a[1] = 0xFF; u8_a.buffer");
Local<v8::SharedArrayBuffer> ab1 = Local<v8::SharedArrayBuffer>::Cast(result);
CheckInternalFieldsAreZero(ab1);
CHECK_EQ(2, ab1->ByteLength());
CHECK(!ab1->IsExternal());
std::shared_ptr<v8::BackingStore> backing_store = Externalize(ab1);
result = CompileRun("ab1.byteLength");
CHECK_EQ(2, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_a[0]");
CHECK_EQ(0xAA, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_a[1]");
CHECK_EQ(0xFF, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8_b = new Uint8Array(ab1);"
"u8_b[0] = 0xBB;"
"u8_a[0]");
CHECK_EQ(0xBB, result->Int32Value(env.local()).FromJust());
result = CompileRun("u8_b[1]");
CHECK_EQ(0xFF, result->Int32Value(env.local()).FromJust());
CHECK_EQ(2, backing_store->ByteLength());
uint8_t* ab1_data = static_cast<uint8_t*>(backing_store->Data());
CHECK_EQ(0xBB, ab1_data[0]);
CHECK_EQ(0xFF, ab1_data[1]);
ab1_data[0] = 0xCC;
ab1_data[1] = 0x11;
result = CompileRun("u8_a[0] + u8_a[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
THREADED_TEST(SharedArrayBuffer_External) {
i::FLAG_harmony_sharedarraybuffer = true;
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
i::ScopedVector<uint8_t> my_data(100);
memset(my_data.begin(), 0, 100);
Local<v8::SharedArrayBuffer> ab3 =
v8::SharedArrayBuffer::New(isolate, my_data.begin(), 100);
CheckInternalFieldsAreZero(ab3);
CHECK_EQ(100, static_cast<int>(ab3->ByteLength()));
CHECK(ab3->IsExternal());
CHECK(env->Global()->Set(env.local(), v8_str("ab3"), ab3).FromJust());
v8::Local<v8::Value> result = CompileRun("ab3.byteLength");
CHECK_EQ(100, result->Int32Value(env.local()).FromJust());
result = CompileRun(
"var u8_b = new Uint8Array(ab3);"
"u8_b[0] = 0xBB;"
"u8_b[1] = 0xCC;"
"u8_b.length");
CHECK_EQ(100, result->Int32Value(env.local()).FromJust());
CHECK_EQ(0xBB, my_data[0]);
CHECK_EQ(0xCC, my_data[1]);
my_data[0] = 0xCC;
my_data[1] = 0x11;
result = CompileRun("u8_b[0] + u8_b[1]");
CHECK_EQ(0xDD, result->Int32Value(env.local()).FromJust());
}
// TODO(v8:9380) the Contents data structure should be deprecated.
THREADED_TEST(SharedArrayBuffer_AllocationInformation) {
i::FLAG_harmony_sharedarraybuffer = true;
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
const size_t ab_size = 1024;
Local<v8::SharedArrayBuffer> ab =
v8::SharedArrayBuffer::New(isolate, ab_size);
v8::SharedArrayBuffer::Contents contents(ab->GetContents());
// Array buffers should have normal allocation mode.
CHECK_EQ(contents.AllocationMode(),
v8::ArrayBuffer::Allocator::AllocationMode::kNormal);
// The allocation must contain the buffer (normally they will be equal, but
// this is not required by the contract).
CHECK_NOT_NULL(contents.AllocationBase());
const uintptr_t alloc =
reinterpret_cast<uintptr_t>(contents.AllocationBase());
const uintptr_t data = reinterpret_cast<uintptr_t>(contents.Data());
CHECK_LE(alloc, data);
CHECK_LE(data + contents.ByteLength(), alloc + contents.AllocationLength());
}
THREADED_TEST(SkipArrayBufferBackingStoreDuringGC) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
// Make sure the pointer looks like a heap object
uint8_t* store_ptr = reinterpret_cast<uint8_t*>(i::kHeapObjectTag);
// Create ArrayBuffer with pointer-that-cannot-be-visited in the backing store
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, store_ptr, 8);
// Should not crash
CcTest::CollectGarbage(i::NEW_SPACE); // in survivor space now
CcTest::CollectGarbage(i::NEW_SPACE); // in old gen now
CcTest::CollectAllGarbage();
CcTest::CollectAllGarbage();
// Should not move the pointer
CHECK_EQ(ab->GetBackingStore()->Data(), store_ptr);
}
THREADED_TEST(SkipArrayBufferDuringScavenge) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
// Make sure the pointer looks like a heap object
Local<v8::Object> tmp = v8::Object::New(isolate);
uint8_t* store_ptr =
reinterpret_cast<uint8_t*>(*reinterpret_cast<uintptr_t*>(*tmp));
// Make `store_ptr` point to from space
CcTest::CollectGarbage(i::NEW_SPACE);
// Create ArrayBuffer with pointer-that-cannot-be-visited in the backing store
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, store_ptr, 8);
// Should not crash,
// i.e. backing store pointer should not be treated as a heap object pointer
CcTest::CollectGarbage(i::NEW_SPACE); // in survivor space now
CcTest::CollectGarbage(i::NEW_SPACE); // in old gen now
// Use `ab` to silence compiler warning
CHECK_EQ(ab->GetBackingStore()->Data(), store_ptr);
}
THREADED_TEST(Regress1006600) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::Value> ab = CompileRunChecked(isolate, "new ArrayBuffer()");
for (int i = 0; i < v8::ArrayBuffer::kEmbedderFieldCount; i++) {
CHECK_NULL(ab.As<v8::Object>()->GetAlignedPointerFromInternalField(i));
}
}
THREADED_TEST(ArrayBuffer_NewBackingStore) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
std::shared_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(isolate, 100);
CHECK(!backing_store->IsShared());
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, backing_store);
CHECK_EQ(backing_store.get(), ab->GetBackingStore().get());
}
THREADED_TEST(SharedArrayBuffer_NewBackingStore) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
std::shared_ptr<v8::BackingStore> backing_store =
v8::SharedArrayBuffer::NewBackingStore(isolate, 100);
CHECK(backing_store->IsShared());
Local<v8::SharedArrayBuffer> ab =
v8::SharedArrayBuffer::New(isolate, backing_store);
CHECK_EQ(backing_store.get(), ab->GetBackingStore().get());
}
static void* backing_store_custom_data = nullptr;
static size_t backing_store_custom_length = 0;
static bool backing_store_custom_called = false;
const intptr_t backing_store_custom_deleter_data = 1234567;
static void BackingStoreCustomDeleter(void* data, size_t length,
void* deleter_data) {
CHECK(!backing_store_custom_called);
CHECK_EQ(backing_store_custom_data, data);
CHECK_EQ(backing_store_custom_length, length);
CHECK_EQ(backing_store_custom_deleter_data,
reinterpret_cast<intptr_t>(deleter_data));
free(data);
backing_store_custom_called = true;
}
TEST(ArrayBuffer_NewBackingStore_CustomDeleter) {
{
// Create and destroy a backing store.
backing_store_custom_called = false;
backing_store_custom_data = malloc(100);
backing_store_custom_length = 100;
v8::ArrayBuffer::NewBackingStore(
backing_store_custom_data, backing_store_custom_length,
BackingStoreCustomDeleter,
reinterpret_cast<void*>(backing_store_custom_deleter_data));
}
CHECK(backing_store_custom_called);
}
TEST(SharedArrayBuffer_NewBackingStore_CustomDeleter) {
{
// Create and destroy a backing store.
backing_store_custom_called = false;
backing_store_custom_data = malloc(100);
backing_store_custom_length = 100;
v8::SharedArrayBuffer::NewBackingStore(
backing_store_custom_data, backing_store_custom_length,
BackingStoreCustomDeleter,
reinterpret_cast<void*>(backing_store_custom_deleter_data));
}
CHECK(backing_store_custom_called);
}
TEST(ArrayBuffer_NewBackingStore_EmptyDeleter) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
char static_buffer[100];
std::unique_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(static_buffer, sizeof(static_buffer),
v8::BackingStore::EmptyDeleter, nullptr);
uint64_t external_memory_before =
isolate->AdjustAmountOfExternalAllocatedMemory(0);
v8::ArrayBuffer::New(isolate, std::move(backing_store));
uint64_t external_memory_after =
isolate->AdjustAmountOfExternalAllocatedMemory(0);
// The ArrayBuffer constructor does not increase the external memory counter.
// The counter may decrease however if the allocation triggers GC.
CHECK_GE(external_memory_before, external_memory_after);
}
TEST(SharedArrayBuffer_NewBackingStore_EmptyDeleter) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
char static_buffer[100];
std::unique_ptr<v8::BackingStore> backing_store =
v8::SharedArrayBuffer::NewBackingStore(
static_buffer, sizeof(static_buffer), v8::BackingStore::EmptyDeleter,
nullptr);
uint64_t external_memory_before =
isolate->AdjustAmountOfExternalAllocatedMemory(0);
v8::SharedArrayBuffer::New(isolate, std::move(backing_store));
uint64_t external_memory_after =
isolate->AdjustAmountOfExternalAllocatedMemory(0);
// The SharedArrayBuffer constructor does not increase the external memory
// counter. The counter may decrease however if the allocation triggers GC.
CHECK_GE(external_memory_before, external_memory_after);
}
THREADED_TEST(BackingStore_NotShared) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 8);
CHECK(!ab->GetBackingStore()->IsShared());
CHECK(!v8::ArrayBuffer::NewBackingStore(isolate, 8)->IsShared());
backing_store_custom_called = false;
backing_store_custom_data = malloc(100);
backing_store_custom_length = 100;
CHECK(!v8::ArrayBuffer::NewBackingStore(
backing_store_custom_data, backing_store_custom_length,
BackingStoreCustomDeleter,
reinterpret_cast<void*>(backing_store_custom_deleter_data))
->IsShared());
}
THREADED_TEST(BackingStore_Shared) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
Local<v8::SharedArrayBuffer> ab = v8::SharedArrayBuffer::New(isolate, 8);
CHECK(ab->GetBackingStore()->IsShared());
CHECK(v8::SharedArrayBuffer::NewBackingStore(isolate, 8)->IsShared());
backing_store_custom_called = false;
backing_store_custom_data = malloc(100);
backing_store_custom_length = 100;
CHECK(v8::SharedArrayBuffer::NewBackingStore(
backing_store_custom_data, backing_store_custom_length,
BackingStoreCustomDeleter,
reinterpret_cast<void*>(backing_store_custom_deleter_data))
->IsShared());
}
[api] Add possibility for BackingStore to keep Allocator alive Add an `array_buffer_allocator_shared` field to the `Isolate::CreateParams` struct that allows embedders to share ownership of the ArrayBuffer::Allocator with V8, and which in particular means that when this method is used that the BackingStore deleter will not perform an use-after-free access to the Allocator under certain circumstances. For Background: tl;dr: This is necessary for Node.js to perform the transition to V8 7.9, because of the way that ArrayBuffer::Allocators and their lifetimes currently work there. In Node.js, each Worker thread has its own ArrayBuffer::Allocator. Changing that would currently be impractical, as each allocator depends on per-Isolate state. However, now that backing stores are managed globally and keep a pointer to the original ArrayBuffer::Allocator, this means that when transferring an ArrayBuffer (e.g. from one Worker to another through postMessage()), the original Allocator has to be kept alive until the ArrayBuffer no longer exists in the receiving Isolate (or until that Isolate is disposed). See [1] for an example Node.js test that fails with V8 7.9. This problem also existed for SharedArrayBuffers, where Node.js was broken by V8 earlier for the same reasons (see [2] for the bug report on that and [3] for the resolution in Node.js). For SharedArrayBuffers, we already had extensive tracking logic, so adding a shared_ptr to keep alive the ArrayBuffer::Allocator was not a significant amount of work. However, the mechanism for transferring non-shared ArrayBuffers is quite different, and it seems both easier for us and better for V8 from an API standpoint to keep the Allocator alive from where it is being referenced. By sharing memory with the custom deleter function/data pair, this comes at no memory overhead. [1]: https://github.com/nodejs/node/pull/30044 [2]: https://github.com/nodejs/node-v8/issues/115 [3]: https://github.com/nodejs/node/pull/29637 Bug: v8:9380 Change-Id: Ibc2c4fb6341b53653cbd637bd8cb3d4ac43809c7 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1874347 Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Reviewed-by: Igor Sheludko <ishell@chromium.org> Cr-Commit-Position: refs/heads/master@{#64542}
2019-10-24 09:45:03 +00:00
THREADED_TEST(ArrayBuffer_NewBackingStore_NullData) {
// This test creates a BackingStore with nullptr as data. The test then
// creates an ArrayBuffer and a TypedArray from this BackingStore. Writing
// into that TypedArray at index 0 is expected to be a no-op, reading from
// that TypedArray at index 0 should result in the default value '0'.
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope handle_scope(isolate);
std::unique_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(nullptr, 0,
v8::BackingStore::EmptyDeleter, nullptr);
v8::Local<v8::ArrayBuffer> buffer =
v8::ArrayBuffer::New(isolate, std::move(backing_store));
CHECK(env->Global()->Set(env.local(), v8_str("buffer"), buffer).FromJust());
v8::Local<v8::Value> result =
CompileRunChecked(isolate,
"const view = new Int32Array(buffer);"
"view[0] = 14;"
"view[0];");
CHECK_EQ(0, result->Int32Value(env.local()).FromJust());
}
[api] Add possibility for BackingStore to keep Allocator alive Add an `array_buffer_allocator_shared` field to the `Isolate::CreateParams` struct that allows embedders to share ownership of the ArrayBuffer::Allocator with V8, and which in particular means that when this method is used that the BackingStore deleter will not perform an use-after-free access to the Allocator under certain circumstances. For Background: tl;dr: This is necessary for Node.js to perform the transition to V8 7.9, because of the way that ArrayBuffer::Allocators and their lifetimes currently work there. In Node.js, each Worker thread has its own ArrayBuffer::Allocator. Changing that would currently be impractical, as each allocator depends on per-Isolate state. However, now that backing stores are managed globally and keep a pointer to the original ArrayBuffer::Allocator, this means that when transferring an ArrayBuffer (e.g. from one Worker to another through postMessage()), the original Allocator has to be kept alive until the ArrayBuffer no longer exists in the receiving Isolate (or until that Isolate is disposed). See [1] for an example Node.js test that fails with V8 7.9. This problem also existed for SharedArrayBuffers, where Node.js was broken by V8 earlier for the same reasons (see [2] for the bug report on that and [3] for the resolution in Node.js). For SharedArrayBuffers, we already had extensive tracking logic, so adding a shared_ptr to keep alive the ArrayBuffer::Allocator was not a significant amount of work. However, the mechanism for transferring non-shared ArrayBuffers is quite different, and it seems both easier for us and better for V8 from an API standpoint to keep the Allocator alive from where it is being referenced. By sharing memory with the custom deleter function/data pair, this comes at no memory overhead. [1]: https://github.com/nodejs/node/pull/30044 [2]: https://github.com/nodejs/node-v8/issues/115 [3]: https://github.com/nodejs/node/pull/29637 Bug: v8:9380 Change-Id: Ibc2c4fb6341b53653cbd637bd8cb3d4ac43809c7 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1874347 Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Reviewed-by: Igor Sheludko <ishell@chromium.org> Cr-Commit-Position: refs/heads/master@{#64542}
2019-10-24 09:45:03 +00:00
class DummyAllocator final : public v8::ArrayBuffer::Allocator {
public:
DummyAllocator() : allocator_(NewDefaultAllocator()) {}
~DummyAllocator() override { CHECK_EQ(allocation_count(), 0); }
void* Allocate(size_t length) override {
allocation_count_++;
return allocator_->Allocate(length);
}
void* AllocateUninitialized(size_t length) override {
allocation_count_++;
return allocator_->AllocateUninitialized(length);
}
void Free(void* data, size_t length) override {
allocation_count_--;
allocator_->Free(data, length);
}
uint64_t allocation_count() const { return allocation_count_; }
private:
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
uint64_t allocation_count_ = 0;
};
TEST(BackingStore_HoldAllocatorAlive_UntilIsolateShutdown) {
std::shared_ptr<DummyAllocator> allocator =
std::make_shared<DummyAllocator>();
std::weak_ptr<DummyAllocator> allocator_weak(allocator);
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator_shared = allocator;
v8::Isolate* isolate = v8::Isolate::New(create_params);
isolate->Enter();
allocator.reset();
create_params.array_buffer_allocator_shared.reset();
CHECK(!allocator_weak.expired());
CHECK_EQ(allocator_weak.lock()->allocation_count(), 0);
{
// Create an ArrayBuffer and do not garbage collect it. This should make
// the allocator be released automatically once the Isolate is disposed.
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(Context::New(isolate));
v8::ArrayBuffer::New(isolate, 8);
// This should be inside the HandleScope, so that we can be sure that
// the allocation is not garbage collected yet.
CHECK(!allocator_weak.expired());
CHECK_EQ(allocator_weak.lock()->allocation_count(), 1);
}
isolate->Exit();
isolate->Dispose();
CHECK(allocator_weak.expired());
}
TEST(BackingStore_HoldAllocatorAlive_AfterIsolateShutdown) {
std::shared_ptr<DummyAllocator> allocator =
std::make_shared<DummyAllocator>();
std::weak_ptr<DummyAllocator> allocator_weak(allocator);
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator_shared = allocator;
v8::Isolate* isolate = v8::Isolate::New(create_params);
isolate->Enter();
allocator.reset();
create_params.array_buffer_allocator_shared.reset();
CHECK(!allocator_weak.expired());
CHECK_EQ(allocator_weak.lock()->allocation_count(), 0);
std::shared_ptr<v8::BackingStore> backing_store;
{
// Create an ArrayBuffer and do not garbage collect it. This should make
// the allocator be released automatically once the Isolate is disposed.
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(Context::New(isolate));
v8::Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 8);
backing_store = ab->GetBackingStore();
}
isolate->Exit();
isolate->Dispose();
CHECK(!allocator_weak.expired());
CHECK_EQ(allocator_weak.lock()->allocation_count(), 1);
backing_store.reset();
CHECK(allocator_weak.expired());
}
class NullptrAllocator final : public v8::ArrayBuffer::Allocator {
public:
void* Allocate(size_t length) override {
CHECK_EQ(length, 0);
return nullptr;
}
void* AllocateUninitialized(size_t length) override {
CHECK_EQ(length, 0);
return nullptr;
}
void Free(void* data, size_t length) override { CHECK_EQ(data, nullptr); }
};
TEST(BackingStore_ReleaseAllocator_NullptrBackingStore) {
std::shared_ptr<NullptrAllocator> allocator =
std::make_shared<NullptrAllocator>();
std::weak_ptr<NullptrAllocator> allocator_weak(allocator);
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator_shared = allocator;
v8::Isolate* isolate = v8::Isolate::New(create_params);
isolate->Enter();
allocator.reset();
create_params.array_buffer_allocator_shared.reset();
CHECK(!allocator_weak.expired());
{
std::shared_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(isolate, 0);
// This should release a reference to the allocator, even though the
// buffer is empty/nullptr.
backing_store.reset();
}
isolate->Exit();
isolate->Dispose();
CHECK(allocator_weak.expired());
}
TEST(BackingStore_ReallocateExpand) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
std::unique_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(isolate, 10);
{
uint8_t* data = reinterpret_cast<uint8_t*>(
reinterpret_cast<uintptr_t>(backing_store->Data()));
for (uint8_t i = 0; i < 10; i++) {
data[i] = i;
}
}
std::unique_ptr<v8::BackingStore> new_backing_store =
v8::BackingStore::Reallocate(isolate, std::move(backing_store), 20);
CHECK_EQ(new_backing_store->ByteLength(), 20);
CHECK(!new_backing_store->IsShared());
{
uint8_t* data = reinterpret_cast<uint8_t*>(
reinterpret_cast<uintptr_t>(new_backing_store->Data()));
for (uint8_t i = 0; i < 10; i++) {
CHECK_EQ(data[i], i);
}
for (uint8_t i = 10; i < 20; i++) {
CHECK_EQ(data[i], 0);
}
}
}
TEST(BackingStore_ReallocateShrink) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
std::unique_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(isolate, 20);
{
uint8_t* data = reinterpret_cast<uint8_t*>(backing_store->Data());
for (uint8_t i = 0; i < 20; i++) {
data[i] = i;
}
}
std::unique_ptr<v8::BackingStore> new_backing_store =
v8::BackingStore::Reallocate(isolate, std::move(backing_store), 10);
CHECK_EQ(new_backing_store->ByteLength(), 10);
CHECK(!new_backing_store->IsShared());
{
uint8_t* data = reinterpret_cast<uint8_t*>(new_backing_store->Data());
for (uint8_t i = 0; i < 10; i++) {
CHECK_EQ(data[i], i);
}
}
}
TEST(BackingStore_ReallocateNotShared) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
std::unique_ptr<v8::BackingStore> backing_store =
v8::ArrayBuffer::NewBackingStore(isolate, 20);
std::unique_ptr<v8::BackingStore> new_backing_store =
v8::BackingStore::Reallocate(isolate, std::move(backing_store), 10);
CHECK(!new_backing_store->IsShared());
}
TEST(BackingStore_ReallocateShared) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
std::unique_ptr<v8::BackingStore> backing_store =
v8::SharedArrayBuffer::NewBackingStore(isolate, 20);
std::unique_ptr<v8::BackingStore> new_backing_store =
v8::BackingStore::Reallocate(isolate, std::move(backing_store), 10);
CHECK(new_backing_store->IsShared());
}