Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions JSTests/stress/array-buffer-external-writers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//@ requireOptions("--useDollarVM=1")
// An embedder can make an ArrayBuffer over memory that something outside JavaScript also writes
// (ArrayBufferExternalWriters::Yes). It says so through canBeWrittenConcurrently(), and the flag goes
// where the bytes go: a transfer keeps it, a copy of the bytes does not have it.

function shouldBe(actual, expected, what) {
if (actual !== expected)
throw new Error(`${what}: expected ${expected} but got ${actual}`);
}
const written = value => $vm.canBeWrittenConcurrently(value);

const buffer = $vm.createArrayBufferWithExternalWriters(16);
shouldBe(Object.getPrototypeOf(buffer), ArrayBuffer.prototype, "an ordinary ArrayBuffer to JavaScript");
shouldBe(buffer.byteLength, 16, "byteLength");
shouldBe(written(buffer), true, "the buffer");
for (const view of [new Uint8Array(buffer), new Uint8Array(buffer, 4, 4), new Float64Array(buffer), new DataView(buffer, 1), new Uint8Array(buffer).subarray(2)])
shouldBe(written(view), true, `a ${view.constructor.name} of it`);

// A copy of the bytes is memory that only JavaScript writes.
shouldBe(written(buffer.slice(0)), false, "ArrayBuffer#slice()");
shouldBe(written(new Uint8Array(buffer).slice()), false, "TypedArray#slice()");
shouldBe(written(new Uint8Array(new Uint8Array(buffer))), false, "a typed array made from a view");

// A transfer moves the bytes, and the flag with them.
const moved = buffer.transfer();
shouldBe(buffer.detached, true, "transfer() detaches");
shouldBe(written(buffer), false, "the detached buffer");
shouldBe(written(moved), true, "transfer()");
const fixed = moved.transferToFixedLength();
shouldBe(written(moved), false, "the buffer that transferToFixedLength() detached");
shouldBe(written(fixed), true, "transferToFixedLength()");
shouldBe(written(new Uint8Array(fixed)), true, "a view of the buffer it moved to");
// A transfer to another length is a copy.
shouldBe(written(fixed.transfer(8)), false, "transfer(8)");

// Memory that the engine allocates has no other writer. A SharedArrayBuffer has.
shouldBe(written(new ArrayBuffer(16)), false, "new ArrayBuffer");
shouldBe(written(new ArrayBuffer(16, { maxByteLength: 32 })), false, "a resizable ArrayBuffer");
shouldBe(written(new Uint8Array(16)), false, "a typed array with no ArrayBuffer");
shouldBe(written(new Uint8Array(2000)), false, "a large typed array with no ArrayBuffer");
shouldBe(written(new Uint8Array(new ArrayBuffer(16))), false, "a view of an ArrayBuffer");
shouldBe(written(new SharedArrayBuffer(16)), true, "a SharedArrayBuffer");
shouldBe(written(new Uint8Array(new SharedArrayBuffer(16))), true, "a view of a SharedArrayBuffer");
shouldBe(written({}), undefined, "not a buffer");
16 changes: 11 additions & 5 deletions Source/JavaScriptCore/runtime/ArrayBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ namespace ArrayBufferInternal {
static constexpr bool verbose = false;
}

// Six words and the flags: a new flag goes next to the others, where it adds nothing to every ArrayBuffer.
static_assert(sizeof(ArrayBufferContents) == 7 * sizeof(void*));

static void zeroFill(void* base, size_t size)
{
constexpr size_t largeZeroFillThreshold = 1 * MB;
Expand Down Expand Up @@ -152,18 +155,19 @@ static RefPtr<BufferMemoryHandle> tryAllocateResizableMemory(VM* vm, size_t size
return adoptRef(*new BufferMemoryHandle(slowMemory, initialBytes, maximumBytes, PageCount::fromBytes(initialBytes), PageCount::fromBytes(maximumBytes), MemorySharingMode::Shared, MemoryMode::BoundsChecking));
}

ArrayBufferContents::ArrayBufferContents(void* data, size_t sizeInBytes, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&& destructor)
ArrayBufferContents::ArrayBufferContents(void* data, size_t sizeInBytes, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&& destructor, ArrayBufferExternalWriters externalWriters)
: m_data(data)
, m_destructor(WTF::move(destructor))
, m_sizeInBytes(sizeInBytes)
, m_maxByteLength(maxByteLength.value_or(sizeInBytes))
, m_hasMaxByteLength(!!maxByteLength)
, m_hasExternalWriters(externalWriters == ArrayBufferExternalWriters::Yes)
{
RELEASE_ASSERT(m_sizeInBytes <= MAX_ARRAY_BUFFER_SIZE);
}

ArrayBufferContents::ArrayBufferContents(std::span<const uint8_t> data, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&& destructor)
: ArrayBufferContents(const_cast<uint8_t*>(data.data()), data.size(), maxByteLength, WTF::move(destructor))
ArrayBufferContents::ArrayBufferContents(std::span<const uint8_t> data, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&& destructor, ArrayBufferExternalWriters externalWriters)
: ArrayBufferContents(const_cast<uint8_t*>(data.data()), data.size(), maxByteLength, WTF::move(destructor), externalWriters)
{
}

Expand Down Expand Up @@ -224,6 +228,7 @@ void ArrayBufferContents::tryAllocate(size_t numElements, unsigned elementByteSi
RELEASE_ASSERT(m_sizeInBytes <= MAX_ARRAY_BUFFER_SIZE);
m_maxByteLength = m_sizeInBytes;
m_hasMaxByteLength = false;
m_hasExternalWriters = false;
m_destructor = ArrayBuffer::primitiveGigacageDestructor();
}

Expand Down Expand Up @@ -265,6 +270,7 @@ void ArrayBufferContents::shareWith(ArrayBufferContents& other) const
other.m_sizeInBytes = m_sizeInBytes;
other.m_maxByteLength = m_maxByteLength;
other.m_hasMaxByteLength = m_hasMaxByteLength;
other.m_hasExternalWriters = m_hasExternalWriters;
RELEASE_ASSERT(other.m_sizeInBytes <= MAX_ARRAY_BUFFER_SIZE);
ASSERT(other.m_maxByteLength <= MAX_ARRAY_BUFFER_SIZE);
}
Expand Down Expand Up @@ -311,12 +317,12 @@ Ref<ArrayBuffer> ArrayBuffer::createAdopted(std::span<const uint8_t> data)
// longer caged, or we could introduce a new set of typed array types that are uncaged and get accessed
// differently.
// - WebAssembly. Wasm should allocate from the cage.
Ref<ArrayBuffer> ArrayBuffer::createFromBytes(std::span<const uint8_t> data, ArrayBufferDestructorFunction&& destructor)
Ref<ArrayBuffer> ArrayBuffer::createFromBytes(std::span<const uint8_t> data, ArrayBufferDestructorFunction&& destructor, ArrayBufferExternalWriters externalWriters)
{
if (data.data() && !Gigacage::isCaged(Gigacage::Primitive, data.data()))
Gigacage::disablePrimitiveGigacage();

ArrayBufferContents contents(data, std::nullopt, WTF::move(destructor));
ArrayBufferContents contents(data, std::nullopt, WTF::move(destructor), externalWriters);
return create(WTF::move(contents));
}

Expand Down
20 changes: 17 additions & 3 deletions Source/JavaScriptCore/runtime/ArrayBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,17 @@ class SharedArrayBufferContents final : public ThreadSafeRefCounted<SharedArrayB
Mode m_mode : 1 { Mode::Default };
};

// Whether something that JavaScript does not run also writes the bytes of an ArrayBuffer that is not shared:
// another process, or a thread outside the engine, that has the same memory. A file mapping is such memory.
// JavaScript sees an ordinary ArrayBuffer either way.
enum class ArrayBufferExternalWriters : bool { No, Yes };

class ArrayBufferContents final {
WTF_MAKE_NONCOPYABLE(ArrayBufferContents);
public:
ArrayBufferContents() = default;
ArrayBufferContents(void* data, size_t sizeInBytes, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&&);
ArrayBufferContents(std::span<const uint8_t> data, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&&);
ArrayBufferContents(void* data, size_t sizeInBytes, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&&, ArrayBufferExternalWriters = ArrayBufferExternalWriters::No);
ArrayBufferContents(std::span<const uint8_t> data, std::optional<size_t> maxByteLength, ArrayBufferDestructorFunction&&, ArrayBufferExternalWriters = ArrayBufferExternalWriters::No);
ArrayBufferContents(Ref<SharedArrayBufferContents>&&, bool forceFixedLengthIfWasm = true);
ArrayBufferContents(void* data, size_t sizeInBytes, size_t maxByteLength, Ref<BufferMemoryHandle>&&);

Expand Down Expand Up @@ -185,6 +190,10 @@ class ArrayBufferContents final {
std::span<const uint8_t> span() const LIFETIME_BOUND { return { static_cast<const uint8_t*>(data()), sizeInBytes() }; }

bool isShared() const { return m_shared; }
// The bytes can change while native code reads them and no JavaScript runs: another thread writes a
// SharedArrayBuffer, and see ArrayBufferExternalWriters. Code that reads a byte twice and counts on
// what it was reads a copy of such bytes.
bool canBeWrittenConcurrently() const { return m_shared || m_hasExternalWriters; }
bool isResizableOrGrowableShared() const { return m_hasMaxByteLength; }
bool isGrowableShared() const { return isResizableOrGrowableShared() && isShared(); }
bool isResizableNonShared() const { return isResizableOrGrowableShared() && !isShared(); }
Expand All @@ -201,6 +210,7 @@ class ArrayBufferContents final {
swap(m_sizeInBytes, other.m_sizeInBytes);
swap(m_maxByteLength, other.m_maxByteLength);
swap(m_hasMaxByteLength, other.m_hasMaxByteLength);
swap(m_hasExternalWriters, other.m_hasExternalWriters);
}

ArrayBufferContents detach()
Expand All @@ -222,6 +232,7 @@ class ArrayBufferContents final {
m_sizeInBytes = 0;
m_maxByteLength = 0;
m_hasMaxByteLength = false;
m_hasExternalWriters = false;
}

friend class ArrayBuffer;
Expand All @@ -244,6 +255,8 @@ class ArrayBufferContents final {
size_t m_sizeInBytes { 0 };
size_t m_maxByteLength { 0 };
bool m_hasMaxByteLength { false };
// It goes where the bytes go: a transfer moves it, and a copy of the bytes does not have it.
bool m_hasExternalWriters { false };
};

class ArrayBuffer final : public GCIncomingRefCounted<ArrayBuffer> {
Expand All @@ -253,7 +266,7 @@ class ArrayBuffer final : public GCIncomingRefCounted<ArrayBuffer> {
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> create(std::span<const uint8_t> = { });
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> create(ArrayBufferContents&&);
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> createAdopted(std::span<const uint8_t>);
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> createFromBytes(std::span<const uint8_t> data, ArrayBufferDestructorFunction&&);
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> createFromBytes(std::span<const uint8_t> data, ArrayBufferDestructorFunction&&, ArrayBufferExternalWriters = ArrayBufferExternalWriters::No);
JS_EXPORT_PRIVATE static Ref<ArrayBuffer> createShared(Ref<SharedArrayBufferContents>&&, bool forceFixedLengthIfWasm = true);
JS_EXPORT_PRIVATE static RefPtr<ArrayBuffer> tryCreate(size_t numElements, unsigned elementByteSize, std::optional<size_t> maxByteLength = std::nullopt);
JS_EXPORT_PRIVATE static RefPtr<ArrayBuffer> tryCreate(ArrayBuffer&);
Expand All @@ -275,6 +288,7 @@ class ArrayBuffer final : public GCIncomingRefCounted<ArrayBuffer> {
void makeShared();
void setSharingMode(ArrayBufferSharingMode);
inline bool isShared() const;
inline bool canBeWrittenConcurrently() const { return m_contents.canBeWrittenConcurrently(); }
inline ArrayBufferSharingMode sharingMode() const { return isShared() ? ArrayBufferSharingMode::Shared : ArrayBufferSharingMode::Default; }
inline bool isResizableOrGrowableShared() const { return m_contents.isResizableOrGrowableShared(); }
inline bool isFixedLength() const { return !isResizableOrGrowableShared(); }
Expand Down
2 changes: 2 additions & 0 deletions Source/JavaScriptCore/runtime/JSArrayBufferView.h
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ class JSArrayBufferView : public JSNonFinalObject {
bool hasArrayBuffer() const { return JSC::hasArrayBuffer(mode()); }

inline bool isShared();
// See ArrayBufferContents::canBeWrittenConcurrently.
inline bool canBeWrittenConcurrently();
JS_EXPORT_PRIVATE ArrayBuffer* unsharedBuffer();
inline ArrayBuffer* possiblySharedBuffer();
JSArrayBuffer* unsharedJSBuffer(JSGlobalObject* globalObject);
Expand Down
21 changes: 21 additions & 0 deletions Source/JavaScriptCore/runtime/JSArrayBufferViewInlinesLight.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ inline bool JSArrayBufferView::isShared()
}
}

inline bool JSArrayBufferView::canBeWrittenConcurrently()
{
switch (m_mode) {
case WastefulTypedArray:
case ResizableNonSharedWastefulTypedArray:
case ResizableNonSharedAutoLengthWastefulTypedArray:
case GrowableSharedWastefulTypedArray:
case GrowableSharedAutoLengthWastefulTypedArray:
return existingBufferInButterfly()->canBeWrittenConcurrently();
case DataViewMode:
case ResizableNonSharedDataViewMode:
case ResizableNonSharedAutoLengthDataViewMode:
case GrowableSharedDataViewMode:
case GrowableSharedAutoLengthDataViewMode:
return uncheckedDowncast<JSDataView>(this)->possiblySharedBuffer()->canBeWrittenConcurrently();
default:
// Only an ArrayBuffer says who else writes its bytes, and this view has none yet.
return false;
}
}

template<JSArrayBufferView::Requester requester>
inline ArrayBuffer* JSArrayBufferView::possiblySharedBufferImpl()
{
Expand Down
38 changes: 38 additions & 0 deletions Source/JavaScriptCore/tools/JSDollarVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
#include "WasmCapabilities.h"
#if USE(BUN_JSC_ADDITIONS)
#include "BufferAccessorRegistry.h"
#include "JSArrayBuffer.h"
#include "JSArrayBufferView.h"
#include "JSBigInt.h"
#include "MathCommon.h"
Expand Down Expand Up @@ -2358,6 +2359,8 @@ static JSC_DECLARE_HOST_FUNCTION(functionFFIWrite);
static JSC_DECLARE_HOST_FUNCTION(functionFFICString);
static JSC_DECLARE_HOST_FUNCTION(functionFFIArenaDepth);
static JSC_DECLARE_HOST_FUNCTION(functionFFICompileCounts);
static JSC_DECLARE_HOST_FUNCTION(functionCreateArrayBufferWithExternalWriters);
static JSC_DECLARE_HOST_FUNCTION(functionCanBeWrittenConcurrently);
#endif

const ClassInfo JSDollarVM::s_info = { "DollarVM"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDollarVM) };
Expand Down Expand Up @@ -5433,6 +5436,39 @@ JSC_DEFINE_HOST_FUNCTION(functionFFIArenaDepth, (JSGlobalObject* globalObject, C
return JSValue::encode(jsNumber(globalObject->ffiContext().arena().depth()));
}

// Makes a zeroed ArrayBuffer with ArrayBufferExternalWriters::Yes, as an embedder does over memory that another process also writes.
// Usage: $vm.createArrayBufferWithExternalWriters(byteLength)
JSC_DEFINE_HOST_FUNCTION(functionCreateArrayBufferWithExternalWriters, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

uint32_t byteLength = callFrame->argument(0).toUInt32(globalObject);
RETURN_IF_EXCEPTION(scope, { });
// At least a byte: null data is a detached buffer.
void* data = Gigacage::tryZeroedMalloc(Gigacage::Primitive, std::max<size_t>(byteLength, 1));
if (!data) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return { };
}
auto buffer = ArrayBuffer::createFromBytes({ static_cast<const uint8_t*>(data), byteLength }, ArrayBuffer::primitiveGigacageDestructor(), ArrayBufferExternalWriters::Yes);
return JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Default), WTF::move(buffer)));
}

// Returns canBeWrittenConcurrently() of an ArrayBuffer or of a view, and undefined for anything else.
// Usage: $vm.canBeWrittenConcurrently(arrayBufferOrView)
JSC_DEFINE_HOST_FUNCTION(functionCanBeWrittenConcurrently, (JSGlobalObject*, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
JSValue value = callFrame->argument(0);
if (auto* view = dynamicDowncast<JSArrayBufferView>(value))
return JSValue::encode(jsBoolean(view->canBeWrittenConcurrently()));
if (auto* buffer = dynamicDowncast<JSArrayBuffer>(value))
return JSValue::encode(jsBoolean(buffer->impl()->canBeWrittenConcurrently()));
return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(functionFFIFixture, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
Expand Down Expand Up @@ -6148,6 +6184,8 @@ void JSDollarVM::finishCreation(VM& vm)
addFunction(vm, allowIfNotFuzz, "ffiCString"_s, functionFFICString, 1);
addFunction(vm, allowIfNotFuzz, "ffiArenaDepth"_s, functionFFIArenaDepth, 0);
addFunction(vm, allowIfNotFuzz, "ffiCompileCounts"_s, functionFFICompileCounts, 0);
addFunction(vm, allowIfNotFuzz, "createArrayBufferWithExternalWriters"_s, functionCreateArrayBufferWithExternalWriters, 1);
addFunction(vm, alwaysAllow, "canBeWrittenConcurrently"_s, functionCanBeWrittenConcurrently, 1);
#endif

if (allowIfNotFuzz) {
Expand Down
Loading