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
50 changes: 13 additions & 37 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,14 @@ const {
copyArrayBuffer,
} = internalBinding('buffer');

const {
arrayBufferViewGetBuffer: ArrayBufferViewGetBuffer,
arrayBufferViewGetByteLength: ArrayBufferViewGetByteLength,
arrayBufferViewGetByteOffset: ArrayBufferViewGetByteOffset,
canCopyArrayBuffer,
cloneAsUint8Array,
} = internalBinding('webstreams');

const {
inspect,
} = require('util');
Expand Down Expand Up @@ -87,38 +90,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);
}
// ArrayBufferViewGetBuffer/ByteLength/ByteOffset, 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
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
145 changes: 145 additions & 0 deletions src/node_webstreams.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "node_binding.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::Object;
using v8::SharedArrayBuffer;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

namespace {

inline bool BufferIsDetached(Local<Value> buffer) {
if (buffer->IsArrayBuffer()) {
return buffer.As<ArrayBuffer>()->WasDetached();
}
// SharedArrayBuffers cannot be detached.
return false;
}

inline size_t BufferByteLength(Local<Value> buffer) {
if (buffer->IsArrayBuffer()) {
return buffer.As<ArrayBuffer>()->ByteLength();
}
return buffer.As<SharedArrayBuffer>()->ByteLength();
}

} // namespace

// Equivalent to:
// Reflect.get(view.constructor.prototype, 'buffer', view)
// but uses the V8 API directly so it is immune to prototype tampering and
// avoids the JS-side overhead of the defensive accessor in lib/internal/.
void ArrayBufferViewGetBuffer(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsArrayBufferView());
args.GetReturnValue().Set(args[0].As<ArrayBufferView>()->Buffer());
}

void ArrayBufferViewGetByteLength(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
args.GetReturnValue().Set(static_cast<double>(view->ByteLength()));
}

void ArrayBufferViewGetByteOffset(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
args.GetReturnValue().Set(static_cast<double>(view->ByteOffset()));
}

// 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());

Local<Object> to_buffer = args[0].As<Object>();
Local<Object> from_buffer = args[2].As<Object>();

if (to_buffer->StrictEquals(from_buffer)) {
args.GetReturnValue().Set(false);
return;
}
if (BufferIsDetached(args[0]) || BufferIsDetached(args[2])) {
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 = BufferByteLength(args[0]);
size_t from_byte_length = BufferByteLength(args[2]);

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) {
Isolate* isolate = args.GetIsolate();
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
size_t byte_length = view->ByteLength();
Local<ArrayBuffer> new_buffer = ArrayBuffer::New(isolate, byte_length);
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, "arrayBufferViewGetBuffer",
ArrayBufferViewGetBuffer);
SetMethod(context, target, "arrayBufferViewGetByteLength",
ArrayBufferViewGetByteLength);
SetMethod(context, target, "arrayBufferViewGetByteOffset",
ArrayBufferViewGetByteOffset);
SetMethod(context, target, "canCopyArrayBuffer", CanCopyArrayBuffer);
SetMethod(context, target, "cloneAsUint8Array", CloneAsUint8Array);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(ArrayBufferViewGetBuffer);
registry->Register(ArrayBufferViewGetByteLength);
registry->Register(ArrayBufferViewGetByteOffset);
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