Skip to content

Commit cf3d050

Browse files
committed
quic: copy arraybuffers/views instead of detach
1 parent 30da8e2 commit cf3d050

7 files changed

Lines changed: 263 additions & 155 deletions

File tree

doc/api/quic.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -798,11 +798,12 @@ the datagram ID.
798798

799799
If `datagram` is a string, it will be encoded using the specified `encoding`.
800800

801-
If `datagram` is an `ArrayBufferView`, the underlying `ArrayBuffer` will be
802-
transferred if possible (taking ownership to prevent mutation after send).
803-
If the buffer is not transferable (e.g., a `SharedArrayBuffer` or a view
804-
over a subset of a larger buffer such as a pooled `Buffer`), the data will
805-
be copied instead.
801+
If `datagram` is an `ArrayBufferView`, the bytes are copied into an
802+
internal buffer; the caller's source buffer is unchanged and may be reused
803+
or mutated immediately after the call returns. Callers that want to ensure
804+
their source cannot be mutated after the call (for example, when handing
805+
the buffer off to another async consumer) can call
806+
`ArrayBuffer.prototype.transfer()` themselves before passing the buffer.
806807

807808
If `datagram` is a `Promise`, it will be awaited before sending. If the
808809
session closes while awaiting, `0n` is returned silently (datagrams are
@@ -1644,6 +1645,13 @@ The Writer has the following methods:
16441645
the readable side via `STOP_SENDING`.
16451646
* `desiredSize` — Available capacity in bytes, or `null` if closed/errored.
16461647

1648+
The bytes from each `writeSync()` / `writevSync()` / `write()` / `writev()`
1649+
input chunk are copied into an internal buffer, so the caller's source
1650+
buffer is unchanged and may be reused or mutated immediately after the
1651+
call returns. Callers that want to ensure a source buffer cannot be
1652+
mutated after handing it off can call `ArrayBuffer.prototype.transfer()`
1653+
themselves before passing the buffer.
1654+
16471655
### `stream.setBody(body)`
16481656

16491657
<!-- YAML
@@ -1661,8 +1669,11 @@ The following body source types are supported:
16611669
* `null` — The writable side is closed immediately (FIN sent with no data).
16621670
* `string` — UTF-8 encoded and sent as a single chunk.
16631671
* `ArrayBuffer`, `SharedArrayBuffer`, `ArrayBufferView` — Sent as a single
1664-
chunk. `ArrayBuffer` and `ArrayBufferView` are detached (zero-copy
1665-
transfer) when possible; `SharedArrayBuffer` is always copied.
1672+
chunk. The bytes are copied into an internal buffer, so the caller's
1673+
source buffer is unchanged and may be reused or mutated immediately
1674+
after the call returns. Callers wanting to ensure their source cannot
1675+
be mutated after handing it off can call
1676+
`ArrayBuffer.prototype.transfer()` themselves before passing the buffer.
16661677
* `Blob` — Sent from the Blob's underlying data queue.
16671678
* {FileHandle} — The file contents are read asynchronously via an
16681679
fd-backed data source. The `FileHandle` must be opened for reading

lib/internal/quic/quic.js

Lines changed: 37 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@
55
/* c8 ignore start */
66

77
const {
8-
ArrayBufferPrototypeGetByteLength,
9-
ArrayBufferPrototypeTransfer,
108
ArrayIsArray,
119
ArrayPrototypePush,
1210
BigInt,
13-
DataViewPrototypeGetBuffer,
1411
DataViewPrototypeGetByteLength,
15-
DataViewPrototypeGetByteOffset,
1612
FunctionPrototypeBind,
1713
Number,
1814
ObjectDefineProperties,
@@ -26,10 +22,7 @@ const {
2622
SymbolAsyncIterator,
2723
SymbolDispose,
2824
SymbolIterator,
29-
TypedArrayPrototypeGetBuffer,
3025
TypedArrayPrototypeGetByteLength,
31-
TypedArrayPrototypeGetByteOffset,
32-
TypedArrayPrototypeSlice,
3326
Uint8Array,
3427
} = primordials;
3528

@@ -1073,53 +1066,24 @@ function invokeOnerror(fn, error) {
10731066

10741067
function validateBody(body) {
10751068
if (body === undefined) return body;
1076-
// Transfer ArrayBuffers...
1077-
if (isArrayBuffer(body)) {
1078-
return ArrayBufferPrototypeTransfer(body);
1079-
}
1080-
// With a SharedArrayBuffer, we always copy. We cannot transfer
1081-
// and it's likely unsafe to use the underlying buffer directly.
1082-
if (isSharedArrayBuffer(body)) {
1083-
return TypedArrayPrototypeSlice(new Uint8Array(body));
1084-
}
1085-
if (isArrayBufferView(body)) {
1086-
let size, offset, buffer;
1087-
if (isDataView(body)) {
1088-
size = DataViewPrototypeGetByteLength(body);
1089-
offset = DataViewPrototypeGetByteOffset(body);
1090-
buffer = DataViewPrototypeGetBuffer(body);
1091-
} else {
1092-
size = TypedArrayPrototypeGetByteLength(body);
1093-
offset = TypedArrayPrototypeGetByteOffset(body);
1094-
buffer = TypedArrayPrototypeGetBuffer(body);
1095-
}
1096-
// We have to be careful in this case. If the ArrayBufferView is a
1097-
// subset of the underlying ArrayBuffer, transferring the entire
1098-
// ArrayBuffer could be incorrect if other views are also using it.
1099-
// So if offset > 0 or size != buffer.byteLength, we'll copy the
1100-
// subset into a new ArrayBuffer instead of transferring.
1101-
if (isSharedArrayBuffer(buffer) ||
1102-
offset !== 0 ||
1103-
size !== ArrayBufferPrototypeGetByteLength(buffer)) {
1104-
return TypedArrayPrototypeSlice(
1105-
new Uint8Array(buffer, offset, size));
1106-
}
1107-
// It's still possible that the ArrayBuffer is being used elsewhere,
1108-
// but we really have no way of knowing. We'll just have to trust
1109-
// the caller in this case.
1110-
return new Uint8Array(
1111-
ArrayBufferPrototypeTransfer(buffer), offset, size);
1069+
// ArrayBuffers, SharedArrayBuffers, and ArrayBufferViews are passed
1070+
// through to the C++ layer which copies the bytes into its own
1071+
// BackingStore. Callers can therefore safely reuse or mutate their
1072+
// input buffers after the call returns. Callers that want to ensure
1073+
// their buffer cannot be mutated after handing it off (for example,
1074+
// when sharing the source with another async consumer) can call
1075+
// ArrayBuffer.prototype.transfer() themselves before passing the
1076+
// buffer.
1077+
if (isArrayBuffer(body) ||
1078+
isSharedArrayBuffer(body) ||
1079+
isArrayBufferView(body)) {
1080+
return body;
11121081
}
11131082
if (isBlob(body)) return body[kBlobHandle];
11141083

11151084
// Strings are encoded as UTF-8.
11161085
if (typeof body === 'string') {
1117-
body = Buffer.from(body, 'utf8');
1118-
// Fall through -- Buffer is an ArrayBufferView, handled above.
1119-
return TypedArrayPrototypeSlice(new Uint8Array(
1120-
TypedArrayPrototypeGetBuffer(body),
1121-
TypedArrayPrototypeGetByteOffset(body),
1122-
TypedArrayPrototypeGetByteLength(body)));
1086+
return Buffer.from(body, 'utf8');
11231087
}
11241088

11251089
// FileHandle -- lock it and pass the C++ handle to GetDataQueueFromSource
@@ -1911,11 +1875,12 @@ class QuicStream {
19111875
}
19121876
// When destroying with an error, ensure the peer learns about
19131877
// it via RESET_STREAM. The writer.fail path inside [kFinishClose]
1914-
// emits RESET_STREAM only when a writer was previously created;
1915-
// streams that destroyed without ever accessing stream.writer
1916-
// (e.g. used setBody or never wrote at all) previously left their
1917-
// write side dangling on the wire. The condition gates the
1918-
// emission to error-path destroys with a still-open writable side.
1878+
// emits RESET_STREAM only when a writer has been created;
1879+
// streams that destroy without ever accessing stream.writer
1880+
// (e.g. used setBody or never wrote at all) need an explicit
1881+
// RESET_STREAM here so the write side does not dangle on the
1882+
// wire. The condition gates the emission to error-path destroys
1883+
// with a still-open writable side.
19191884
// Direction model for the writable side:
19201885
// * bidi: always has a writable side.
19211886
// * uni + #reader === undefined: locally-initiated, write-only.
@@ -3165,8 +3130,12 @@ class QuicSession {
31653130
*
31663131
* If a string is given it will be encoded using the specified encoding.
31673132
*
3168-
* If an ArrayBufferView is given, the underlying ArrayBuffer will be
3169-
* transferred if possible, otherwise the data will be copied.
3133+
* If an ArrayBufferView is given, the bytes are copied into an internal
3134+
* buffer; the caller's source buffer is unchanged and may be reused
3135+
* immediately. Callers that want to ensure their source cannot be
3136+
* mutated after the call (for example, when handing the buffer off to
3137+
* another async consumer) can call ArrayBuffer.prototype.transfer()
3138+
* themselves before passing it.
31703139
*
31713140
* If a Promise is given, it will be awaited before sending. If the
31723141
* session closes while awaiting, 0n is returned silently.
@@ -3179,7 +3148,6 @@ class QuicSession {
31793148
if (this.#isClosedOrClosing) {
31803149
throw new ERR_INVALID_STATE('Session is closed');
31813150
}
3182-
let offset, length, buffer;
31833151

31843152
const maxDatagramSize = this.#state.maxDatagramSize;
31853153

@@ -3196,45 +3164,21 @@ class QuicSession {
31963164
}
31973165

31983166
if (typeof datagram === 'string') {
3199-
// Buffer.from may return a pooled buffer that can't be transferred.
3200-
// Slice to get an independent copy.
32013167
datagram = new Uint8Array(Buffer.from(datagram, encoding));
3202-
length = TypedArrayPrototypeGetByteLength(datagram);
3203-
if (length === 0) return kNilDatagramId;
3204-
} else {
3205-
if (!isArrayBufferView(datagram)) {
3206-
throw new ERR_INVALID_ARG_TYPE('datagram',
3207-
['ArrayBufferView', 'string'],
3208-
datagram);
3209-
}
3210-
if (isDataView(datagram)) {
3211-
offset = DataViewPrototypeGetByteOffset(datagram);
3212-
length = DataViewPrototypeGetByteLength(datagram);
3213-
buffer = DataViewPrototypeGetBuffer(datagram);
3214-
} else {
3215-
offset = TypedArrayPrototypeGetByteOffset(datagram);
3216-
length = TypedArrayPrototypeGetByteLength(datagram);
3217-
buffer = TypedArrayPrototypeGetBuffer(datagram);
3218-
}
3219-
3220-
// If the view has zero length (e.g. detached buffer), there's
3221-
// nothing to send.
3222-
if (length === 0) return kNilDatagramId;
3223-
3224-
if (isSharedArrayBuffer(buffer) ||
3225-
offset !== 0 ||
3226-
length !== ArrayBufferPrototypeGetByteLength(buffer)) {
3227-
// Copy if the buffer is not transferable (SharedArrayBuffer)
3228-
// or if the view is over a subset of the buffer (e.g. a
3229-
// Node.js Buffer from the pool).
3230-
datagram = TypedArrayPrototypeSlice(
3231-
new Uint8Array(buffer), offset, offset + length);
3232-
} else {
3233-
datagram = new Uint8Array(
3234-
ArrayBufferPrototypeTransfer(buffer), offset, length);
3235-
}
3168+
} else if (!isArrayBufferView(datagram)) {
3169+
throw new ERR_INVALID_ARG_TYPE('datagram',
3170+
['ArrayBufferView', 'string'],
3171+
datagram);
32363172
}
32373173

3174+
const length = isDataView(datagram) ?
3175+
DataViewPrototypeGetByteLength(datagram) :
3176+
TypedArrayPrototypeGetByteLength(datagram);
3177+
3178+
// If the view has zero length (e.g. detached buffer), there's
3179+
// nothing to send.
3180+
if (length === 0) return kNilDatagramId;
3181+
32383182
// The peer max datagram size is less than the datagram we want to send,
32393183
// so... don't send it.
32403184
if (length > maxDatagramSize) return kNilDatagramId;

src/quic/data.cc

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,31 +88,45 @@ Store::Store(std::unique_ptr<BackingStore> store, size_t length, size_t offset)
8888
CHECK_LE(length_, store_->ByteLength() - offset_);
8989
}
9090

91-
Maybe<Store> Store::From(Local<ArrayBuffer> buffer, Local<Value> detach_key) {
92-
if (!buffer->IsDetachable()) {
93-
return Nothing<Store>();
94-
}
95-
bool res;
96-
auto backing = buffer->GetBackingStore();
91+
Maybe<Store> Store::From(Local<ArrayBuffer> buffer) {
92+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
93+
Environment* env = Environment::GetCurrent(isolate->GetCurrentContext());
9794
auto length = buffer->ByteLength();
98-
if (!buffer->Detach(detach_key).To(&res) || !res) {
95+
auto dest = ArrayBuffer::NewBackingStore(
96+
isolate,
97+
length,
98+
v8::BackingStoreInitializationMode::kUninitialized,
99+
v8::BackingStoreOnFailureMode::kReturnNull);
100+
if (!dest) {
101+
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
99102
return Nothing<Store>();
100103
}
101-
return Just(Store(std::move(backing), length, 0));
104+
if (length > 0) {
105+
memcpy(dest->Data(), buffer->Data(), length);
106+
}
107+
return Just(Store(std::move(dest), length, 0));
102108
}
103109

104-
Maybe<Store> Store::From(Local<ArrayBufferView> view, Local<Value> detach_key) {
105-
if (!view->Buffer()->IsDetachable()) {
106-
return Nothing<Store>();
107-
}
108-
bool res;
109-
auto backing = view->Buffer()->GetBackingStore();
110+
Maybe<Store> Store::From(Local<ArrayBufferView> view) {
111+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
112+
Environment* env = Environment::GetCurrent(isolate->GetCurrentContext());
110113
auto length = view->ByteLength();
111114
auto offset = view->ByteOffset();
112-
if (!view->Buffer()->Detach(detach_key).To(&res) || !res) {
115+
auto dest = ArrayBuffer::NewBackingStore(
116+
isolate,
117+
length,
118+
v8::BackingStoreInitializationMode::kUninitialized,
119+
v8::BackingStoreOnFailureMode::kReturnNull);
120+
if (!dest) {
121+
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
113122
return Nothing<Store>();
114123
}
115-
return Just(Store(std::move(backing), length, offset));
124+
if (length > 0) {
125+
memcpy(dest->Data(),
126+
static_cast<const uint8_t*>(view->Buffer()->Data()) + offset,
127+
length);
128+
}
129+
return Just(Store(std::move(dest), length, 0));
116130
}
117131

118132
Store Store::CopyFrom(Local<ArrayBuffer> buffer) {

src/quic/data.h

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,28 @@ class Store final : public MemoryRetainer {
5454
size_t length,
5555
size_t offset = 0);
5656

57-
// Creates a Store from the contents of an ArrayBuffer, always detaching
58-
// it in the process. An empty Maybe will be returned if the ArrayBuffer
59-
// is not detachable or detaching failed (likely due to a detach key
60-
// mismatch).
61-
static v8::Maybe<Store> From(
62-
v8::Local<v8::ArrayBuffer> buffer,
63-
v8::Local<v8::Value> detach_key = v8::Local<v8::Value>());
64-
65-
// Creates a Store from the contents of an ArrayBufferView, always detaching
66-
// it in the process. An empty Maybe will be returned if the ArrayBuffer
67-
// is not detachable or detaching failed (likely due to a detach key
68-
// mismatch).
69-
static v8::Maybe<Store> From(
70-
v8::Local<v8::ArrayBufferView> view,
71-
v8::Local<v8::Value> detach_key = v8::Local<v8::Value>());
57+
// Creates a Store by copying the contents of an ArrayBuffer into a fresh
58+
// BackingStore. The caller's buffer is not modified, so callers can safely
59+
// reuse or mutate it after the call returns. Returns an empty Maybe on
60+
// allocation failure, in which case an `ERR_MEMORY_ALLOCATION_FAILED`
61+
// exception will have been scheduled on the isolate.
62+
static v8::Maybe<Store> From(v8::Local<v8::ArrayBuffer> buffer);
63+
64+
// Creates a Store by copying the contents of an ArrayBufferView into a
65+
// fresh BackingStore. The caller's view (and its underlying ArrayBuffer)
66+
// is not modified. Returns an empty Maybe on allocation failure, in which
67+
// case an `ERR_MEMORY_ALLOCATION_FAILED` exception will have been
68+
// scheduled on the isolate.
69+
static v8::Maybe<Store> From(v8::Local<v8::ArrayBufferView> view);
7270

7371
// Creates a Store from the contents of an ArrayBuffer, always copying the
74-
// content.
72+
// content. Equivalent to `From()` but returns a Store directly without
73+
// surfacing allocation failure.
7574
static Store CopyFrom(v8::Local<v8::ArrayBuffer> buffer);
7675

77-
// Creates a Store from the contents of an ArrayBufferView, always copying the
78-
// content.
76+
// Creates a Store from the contents of an ArrayBufferView, always copying
77+
// the content. Equivalent to `From()` but returns a Store directly without
78+
// surfacing allocation failure.
7979
static Store CopyFrom(v8::Local<v8::ArrayBufferView> view);
8080

8181
v8::Local<v8::Uint8Array> ToUint8Array(Environment* env) const;

src/quic/streams.cc

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -154,31 +154,23 @@ STAT_STRUCT(Stream, STREAM)
154154
// ============================================================================
155155

156156
namespace {
157-
// Creates an in-memory DataQueue entry from an ArrayBuffer by either
158-
// detaching it (zero-copy) or copying its contents if detach is not
159-
// possible (e.g., SharedArrayBuffer-backed or non-detachable).
160-
// Returns nullptr on failure (error already thrown if allocation failed).
157+
// Creates an in-memory DataQueue entry by copying the requested range of
158+
// the given ArrayBuffer into a fresh BackingStore. The caller's buffer is
159+
// not detached or otherwise modified, so callers can safely reuse or
160+
// mutate it after the call returns. Callers that want to ensure their
161+
// buffer cannot be mutated after handing it off can call
162+
// `ArrayBuffer.prototype.transfer()` themselves before calling into the
163+
// QUIC API.
164+
// Returns nullptr on zero length or allocation failure.
161165
std::unique_ptr<DataQueue::Entry> CreateEntryFromBuffer(
162166
Environment* env, Local<ArrayBuffer> buffer, size_t offset, size_t length) {
163167
if (length == 0) return nullptr;
164-
std::shared_ptr<BackingStore> backing;
165-
if (buffer->IsDetachable()) {
166-
backing = buffer->GetBackingStore();
167-
if (buffer->Detach(Local<Value>()).IsNothing()) {
168-
backing.reset();
169-
}
170-
}
171-
if (!backing) {
172-
// Buffer is not detachable or detach failed. Copy the data.
173-
JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr);
174-
memcpy(copy->Data(),
175-
static_cast<const uint8_t*>(buffer->Data()) + offset,
176-
length);
177-
offset = 0;
178-
backing = std::move(copy);
179-
}
168+
JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr);
169+
memcpy(copy->Data(),
170+
static_cast<const uint8_t*>(buffer->Data()) + offset,
171+
length);
180172
return DataQueue::CreateInMemoryEntryFromBackingStore(
181-
std::move(backing), offset, length);
173+
std::move(copy), 0, length);
182174
}
183175
} // namespace
184176

0 commit comments

Comments
 (0)