Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ext/web): use ArrayBuffer.was_detached() #16307

Merged
merged 6 commits into from
Oct 25, 2022
Merged
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
23 changes: 23 additions & 0 deletions cli/tests/unit/structured_clone_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,27 @@ Deno.test("correct DataCloneError message", () => {
DOMException,
"Value not transferable",
);

const ab = new ArrayBuffer(1);
// detach ArrayBuffer
structuredClone(ab, { transfer: [ab] });
assertThrows(
() => {
structuredClone(ab, { transfer: [ab] });
},
DOMException,
"ArrayBuffer at index 0 is already detached",
);

const ab2 = new ArrayBuffer(0);
assertThrows(
() => {
structuredClone([ab2, ab], { transfer: [ab2, ab] });
},
DOMException,
"ArrayBuffer at index 1 is already detached",
);

// ab2 should not be detached after above failure
structuredClone(ab2, { transfer: [ab2] });
});
26 changes: 21 additions & 5 deletions core/ops_builtin_v8.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use crate::bindings::script_origin;
use crate::error::custom_error;
use crate::error::is_instance_of_error;
use crate::error::range_error;
use crate::error::type_error;
Expand Down Expand Up @@ -52,6 +53,7 @@ pub(crate) fn init_builtins_v8() -> Vec<OpDecl> {
op_store_pending_promise_exception::decl(),
op_remove_pending_promise_exception::decl(),
op_has_pending_promise_exception::decl(),
op_arraybuffer_was_detached::decl(),
]
}

Expand Down Expand Up @@ -449,22 +451,27 @@ fn op_serialize(
if let Some(transferred_array_buffers) = transferred_array_buffers {
let state_rc = JsRuntime::state(scope);
let state = state_rc.borrow_mut();
for i in 0..transferred_array_buffers.length() {
let i = v8::Number::new(scope, i as f64).into();
for index in 0..transferred_array_buffers.length() {
let i = v8::Number::new(scope, index as f64).into();
let buf = transferred_array_buffers.get(scope, i).unwrap();
let buf = v8::Local::<v8::ArrayBuffer>::try_from(buf).map_err(|_| {
type_error("item in transferredArrayBuffers not an ArrayBuffer")
})?;
if let Some(shared_array_buffer_store) = &state.shared_array_buffer_store
{
// TODO(lucacasonato): we need to check here that the buffer is not
// already detached. We can not do that because V8 does not provide
// a way to check if a buffer is already detached.
if !buf.is_detachable() {
return Err(type_error(
"item in transferredArrayBuffers is not transferable",
));
}

if buf.was_detached() {
return Err(custom_error(
"DOMExceptionOperationError",
format!("ArrayBuffer at index {} is already detached", index),
));
}

let backing_store = buf.get_backing_store();
buf.detach();
let id = shared_array_buffer_store.insert(backing_store);
Expand Down Expand Up @@ -887,3 +894,12 @@ fn op_has_pending_promise_exception<'a>(
.pending_promise_exceptions
.contains_key(&promise_global)
}

#[op(v8)]
fn op_arraybuffer_was_detached<'a>(
_scope: &mut v8::HandleScope<'a>,
input: serde_v8::Value<'a>,
) -> Result<bool, Error> {
let ab = v8::Local::<v8::ArrayBuffer>::try_from(input.v8_value)?;
Ok(ab.was_detached())
}
8 changes: 7 additions & 1 deletion ext/web/06_streams.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,13 @@
* @returns {boolean}
*/
function isDetachedBuffer(O) {
return ReflectHas(O, isFakeDetached);
if (O.byteLength !== 0) {
return false;
}
// TODO(marcosc90) remove isFakeDetached once transferArrayBuffer
// actually detaches the buffer
return ReflectHas(O, isFakeDetached) ||
core.ops.op_arraybuffer_was_detached(O);
Copy link
Contributor Author

@marcosc90 marcosc90 Oct 20, 2022

Choose a reason for hiding this comment

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

This can be changed after is merged #16294 to:

return core.ops.op_arraybuffer_was_detached(O);

}

/**
Expand Down
32 changes: 12 additions & 20 deletions ext/web/13_message_port.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
SymbolFor,
SymbolIterator,
TypeError,
WeakSet,
WeakSetPrototypeAdd,
WeakSetPrototypeHas,
} = window.__bootstrap.primordials;

class MessageChannel {
Expand Down Expand Up @@ -239,30 +236,25 @@
return [data, transferables];
}

const detachedArrayBuffers = new WeakSet();

/**
* @param {any} data
* @param {object[]} transferables
* @returns {globalThis.__bootstrap.messagePort.MessageData}
*/
function serializeJsMessageData(data, transferables) {
const transferredArrayBuffers = ArrayPrototypeFilter(
transferables,
(a) => ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, a),
);

for (const arrayBuffer of transferredArrayBuffers) {
// This is hacky with both false positives and false negatives for
// detecting detached array buffers. V8 needs to add a way to tell if a
// buffer is detached or not.
if (WeakSetPrototypeHas(detachedArrayBuffers, arrayBuffer)) {
throw new DOMException(
"Can not transfer detached ArrayBuffer",
"DataCloneError",
);
const transferredArrayBuffers = [];
for (let i = 0, j = 0; i < transferables.length; i++) {
const ab = transferables[i];
if (ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, ab)) {
if (ab.byteLength === 0 && core.ops.op_arraybuffer_was_detached(ab)) {
throw new DOMException(
`ArrayBuffer at index ${j} is already detached`,
"DataCloneError",
);
}
j++;
transferredArrayBuffers.push(ab);
Comment on lines +246 to +256
Copy link
Member

@lucacasonato lucacasonato Oct 24, 2022

Choose a reason for hiding this comment

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

Suggested change
for (let i = 0, j = 0; i < transferables.length; i++) {
const ab = transferables[i];
if (ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, ab)) {
if (ab.byteLength === 0 && core.ops.op_arraybuffer_was_detached(ab)) {
throw new DOMException(
`ArrayBuffer at index ${j} is already detached`,
"DataCloneError",
);
}
j++;
transferredArrayBuffers.push(ab);
for (let i = 0; i < transferables.length; i++) {
const ab = transferables[i];
if (ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, ab)) {
if (ab.byteLength === 0 && core.ops.op_arraybuffer_was_detached(ab)) {
throw new DOMException(
`ArrayBuffer at index ${i} is already detached`,
"DataCloneError",
);
}
transferredArrayBuffers.push(ab);

The user doesn't care about the index of the AB, they care about the index in the transferables array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lucacasonato Wanted to mimic Chrome error message.

const buf = new Uint8Array(5);
structuredClone(buf, { transfer: [buf.buffer] });
structuredClone([buf.buffer], { transfer: [new ReadableStream(), buf.buffer] });
// Uncaught DOMException: ... ArrayBuffer at index 0 is already detached

Also did it that way to make JS error be the same as Rust's where we're looping through AB list, not transferables

format!("ArrayBuffer at index {} is already detached", index),

Since we're not passing the transferables array to Rust, should I just remove the index hint, and leave the old message: Can not transfer detached ArrayBuffer?

Copy link
Member

Choose a reason for hiding this comment

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

Okay, I think mimicing Chrome is fine here.

}
WeakSetPrototypeAdd(detachedArrayBuffers, arrayBuffer);
}

const serializedData = core.serialize(data, {
Expand Down