Skip to content
Open
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
38 changes: 20 additions & 18 deletions lib/internal/webstreams/readablestream.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,6 @@ const {
} = require('internal/streams/utils');

const {
ArrayBufferViewGetBuffer,
ArrayBufferViewGetByteLength,
ArrayBufferViewGetByteOffset,
AsyncIterator,
canCopyArrayBuffer,
cloneAsUint8Array,
Expand All @@ -106,6 +103,7 @@ const {
enqueueValueWithSize,
extractHighWaterMark,
extractSizeAlgorithm,
getArrayBufferView,
getNonWritablePropertyDescriptor,
isBrandCheck,
kState,
Expand Down Expand Up @@ -688,8 +686,10 @@ class ReadableStreamBYOBRequest {
'This BYOB request has been invalidated');
}

const viewByteLength = ArrayBufferViewGetByteLength(view);
const viewBuffer = ArrayBufferViewGetBuffer(view);
const {
buffer: viewBuffer,
byteLength: viewByteLength,
} = getArrayBufferView(view);
const viewBufferByteLength = ArrayBufferPrototypeGetByteLength(viewBuffer);

if (ArrayBufferPrototypeGetDetached(viewBuffer)) {
Expand Down Expand Up @@ -980,8 +980,10 @@ class ReadableStreamBYOBReader {
}
validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);

const viewByteLength = ArrayBufferViewGetByteLength(view);
const viewBuffer = ArrayBufferViewGetBuffer(view);
const {
buffer: viewBuffer,
byteLength: viewByteLength,
} = getArrayBufferView(view);

if (isSharedArrayBuffer(viewBuffer)) {
throw new ERR_INVALID_ARG_VALUE(
Expand Down Expand Up @@ -1198,8 +1200,10 @@ class ReadableByteStreamController {
if (!isReadableByteStreamController(this))
throw new ERR_INVALID_THIS('ReadableByteStreamController');
validateBuffer(chunk);
const chunkByteLength = ArrayBufferViewGetByteLength(chunk);
const chunkBuffer = ArrayBufferViewGetBuffer(chunk);
const {
buffer: chunkBuffer,
byteLength: chunkByteLength,
} = getArrayBufferView(chunk);

if (isSharedArrayBuffer(chunkBuffer)) {
throw new ERR_INVALID_ARG_VALUE(
Expand Down Expand Up @@ -2745,9 +2749,7 @@ function readableByteStreamControllerPullInto(
assert(minimumFill >= elementSize && minimumFill <= view.byteLength);
assert(minimumFill % elementSize === 0);

const buffer = ArrayBufferViewGetBuffer(view);
const byteOffset = ArrayBufferViewGetByteOffset(view);
const byteLength = ArrayBufferViewGetByteLength(view);
const { buffer, byteOffset, byteLength } = getArrayBufferView(view);
const bufferByteLength = ArrayBufferPrototypeGetByteLength(buffer);

let transferredBuffer;
Expand Down Expand Up @@ -2888,9 +2890,7 @@ function readableByteStreamControllerEnqueue(controller, chunk) {
stream,
} = controller[kState];

const buffer = ArrayBufferViewGetBuffer(chunk);
const byteOffset = ArrayBufferViewGetByteOffset(chunk);
const byteLength = ArrayBufferViewGetByteLength(chunk);
const { buffer, byteOffset, byteLength } = getArrayBufferView(chunk);

if (closeRequested || stream[kState].state !== 'readable')
return;
Expand Down Expand Up @@ -3183,9 +3183,11 @@ function readableByteStreamControllerRespondWithNewView(controller, view) {
const desc = pendingPullIntos[0];
assert(stream[kState].state !== 'errored');

const viewByteLength = ArrayBufferViewGetByteLength(view);
const viewByteOffset = ArrayBufferViewGetByteOffset(view);
const viewBuffer = ArrayBufferViewGetBuffer(view);
const {
buffer: viewBuffer,
byteOffset: viewByteOffset,
byteLength: viewByteLength,
} = getArrayBufferView(view);
const viewBufferByteLength = ArrayBufferPrototypeGetByteLength(viewBuffer);

if (stream[kState].state === 'closed') {
Expand Down
52 changes: 12 additions & 40 deletions lib/internal/webstreams/util.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
'use strict';

const {
ArrayBufferPrototypeGetByteLength,
ArrayBufferPrototypeGetDetached,
ArrayBufferPrototypeSlice,
ArrayPrototypePush,
ArrayPrototypeShift,
AsyncIteratorPrototype,
MathMax,
NumberIsNaN,
PromisePrototypeThen,
ReflectApply,
ReflectGet,
Symbol,
Uint8Array,
} = primordials;

const {
Expand All @@ -26,6 +21,12 @@ const {
copyArrayBuffer,
} = internalBinding('buffer');

const {
canCopyArrayBuffer,
cloneAsUint8Array,
getArrayBufferView,
} = internalBinding('webstreams');

const {
inspect,
} = require('util');
Expand Down Expand Up @@ -87,38 +88,11 @@ function customInspect(depth, options, name, data) {
return `${name} ${inspect(data, opts)}`;
}

// These are defensive to work around the possibility that
// the buffer, byteLength, and byteOffset properties on
// ArrayBuffer and ArrayBufferView's may have been tampered with.

function ArrayBufferViewGetBuffer(view) {
return ReflectGet(view.constructor.prototype, 'buffer', view);
}

function ArrayBufferViewGetByteLength(view) {
return ReflectGet(view.constructor.prototype, 'byteLength', view);
}

function ArrayBufferViewGetByteOffset(view) {
return ReflectGet(view.constructor.prototype, 'byteOffset', view);
}

function cloneAsUint8Array(view) {
const buffer = ArrayBufferViewGetBuffer(view);
const byteOffset = ArrayBufferViewGetByteOffset(view);
const byteLength = ArrayBufferViewGetByteLength(view);
return new Uint8Array(
ArrayBufferPrototypeSlice(buffer, byteOffset, byteOffset + byteLength),
);
}

function canCopyArrayBuffer(toBuffer, toIndex, fromBuffer, fromIndex, count) {
return toBuffer !== fromBuffer &&
!ArrayBufferPrototypeGetDetached(toBuffer) &&
!ArrayBufferPrototypeGetDetached(fromBuffer) &&
toIndex + count <= ArrayBufferPrototypeGetByteLength(toBuffer) &&
fromIndex + count <= ArrayBufferPrototypeGetByteLength(fromBuffer);
}
// getArrayBufferView, canCopyArrayBuffer, and cloneAsUint8Array are
// implemented in src/node_webstreams.cc via direct V8 API calls. They are
// immune to user tampering of typed-array prototypes (matching the defensive
// behavior of the previous Reflect.get-based JS implementation) and faster on
// hot byte-stream paths.

function isBrandCheck(brand) {
return (value) => {
Expand Down Expand Up @@ -206,9 +180,6 @@ function lazyTransfer() {
}

module.exports = {
ArrayBufferViewGetBuffer,
ArrayBufferViewGetByteLength,
ArrayBufferViewGetByteOffset,
AsyncIterator,
canCopyArrayBuffer,
cloneAsUint8Array,
Expand All @@ -219,6 +190,7 @@ module.exports = {
enqueueValueWithSize,
extractHighWaterMark,
extractSizeAlgorithm,
getArrayBufferView,
getNonWritablePropertyDescriptor,
isBrandCheck,
isPromisePending,
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
'src/node_types.cc',
'src/node_url.cc',
'src/node_url_pattern.cc',
'src/node_webstreams.cc',
'src/node_util.cc',
'src/node_v8.cc',
'src/node_wasi.cc',
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
V(v8) \
V(wasi) \
V(wasm_web_api) \
V(webstreams) \
V(watchdog) \
V(worker) \
V(zlib)
Expand Down
1 change: 1 addition & 0 deletions src/node_external_reference.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class ExternalReferenceRegistry {
V(v8) \
V(zlib) \
V(wasm_web_api) \
V(webstreams) \
V(worker)

#if NODE_HAVE_I18N_SUPPORT
Expand Down
134 changes: 134 additions & 0 deletions src/node_webstreams.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#include "env-inl.h"
#include "node_binding.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "util-inl.h"
#include "v8.h"

namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

// Returns { buffer, byteOffset, byteLength } in a single binding crossing,
// equivalent to reading the three properties via
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "byteLength"),
};
Local<Value> values[] = {
view->Buffer(),
Number::New(isolate, static_cast<double>(view->ByteOffset())),
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));
}

// Returns true iff bytes can be safely copied between the buffers given the
// requested offsets and count. Matches lib/internal/webstreams/util.js:
// toBuffer !== fromBuffer &&
// !toBuffer.detached &&
// !fromBuffer.detached &&
// toIndex + count <= toBuffer.byteLength &&
// fromIndex + count <= fromBuffer.byteLength
void CanCopyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

// SharedArrayBuffer handles are interoperable with ArrayBuffer handles in
// V8, so we can use the ArrayBuffer accessors uniformly. WasDetached()
// always returns false on a SAB.
Local<ArrayBuffer> to_buffer = args[0].As<ArrayBuffer>();
Local<ArrayBuffer> from_buffer = args[2].As<ArrayBuffer>();

if (to_buffer->StrictEquals(from_buffer)) {
args.GetReturnValue().Set(false);
return;
}
if (to_buffer->WasDetached() || from_buffer->WasDetached()) {
args.GetReturnValue().Set(false);
return;
}

uint32_t to_index = args[1].As<Uint32>()->Value();
uint32_t from_index = args[3].As<Uint32>()->Value();
uint32_t count = args[4].As<Uint32>()->Value();

size_t to_byte_length = to_buffer->ByteLength();
size_t from_byte_length = from_buffer->ByteLength();

bool ok = static_cast<uint64_t>(to_index) + count <= to_byte_length &&
static_cast<uint64_t>(from_index) + count <= from_byte_length;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this guard against overflows?

args.GetReturnValue().Set(ok);
}

// Equivalent to:
// new Uint8Array(view.buffer.slice(view.byteOffset,
// view.byteOffset + view.byteLength))
// Allocates a fresh ArrayBuffer with the view's bytes copied into it, then
// returns a Uint8Array over the full new buffer. Avoids the JS-side
// Reflect.get + slice round-trip.
void CloneAsUint8Array(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
size_t byte_length = view->ByteLength();
Local<ArrayBuffer> new_buffer;
if (!ArrayBuffer::MaybeNew(isolate, byte_length).ToLocal(&new_buffer)) {
// MaybeNew does not schedule an exception on allocation failure.
env->ThrowRangeError("Array buffer allocation failed");
return;
}
if (byte_length > 0) {
size_t copied = view->CopyContents(new_buffer->Data(), byte_length);
CHECK_EQ(copied, byte_length);
}
args.GetReturnValue().Set(Uint8Array::New(new_buffer, 0, byte_length));
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetMethod(context, target, "getArrayBufferView", GetArrayBufferView);
SetMethod(context, target, "canCopyArrayBuffer", CanCopyArrayBuffer);
SetMethod(context, target, "cloneAsUint8Array", CloneAsUint8Array);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetArrayBufferView);
registry->Register(CanCopyArrayBuffer);
registry->Register(CloneAsUint8Array);
}

} // namespace webstreams
} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(webstreams, node::webstreams::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(webstreams,
node::webstreams::RegisterExternalReferences)
Loading