diff options
author | Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> | 2024-09-10 14:50:21 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-10 21:50:21 +0000 |
commit | be0ba6d84f190f4fc1b4517e62d9d8ad30c8cfb1 (patch) | |
tree | 7e088e66f70d69e7074ff20fb192e73a661facfa /ext/node | |
parent | e522f4b65a3439030506733b104498f21422ede0 (diff) |
fix(ext/node): Rewrite `node:v8` serialize/deserialize (#25439)
Closes #20613.
Reimplements the serialization on top of the v8 APIs instead of
deno_core. Implements `v8.Serializer`, `v8.DefaultSerializer`,
`v8.Deserializer`, and `v8.DefaultSerializer`.
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/lib.rs | 19 | ||||
-rw-r--r-- | ext/node/ops/v8.rs | 361 | ||||
-rw-r--r-- | ext/node/polyfills/v8.ts | 235 |
3 files changed, 588 insertions, 27 deletions
diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f569f5b2a..a589a99be 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -286,6 +286,25 @@ deno_core::extension!(deno_node, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, ops::v8::op_v8_get_heap_statistics, + ops::v8::op_v8_get_wire_format_version, + ops::v8::op_v8_new_deserializer, + ops::v8::op_v8_new_serializer, + ops::v8::op_v8_read_double, + ops::v8::op_v8_read_header, + ops::v8::op_v8_read_raw_bytes, + ops::v8::op_v8_read_uint32, + ops::v8::op_v8_read_uint64, + ops::v8::op_v8_read_value, + ops::v8::op_v8_release_buffer, + ops::v8::op_v8_set_treat_array_buffer_views_as_host_objects, + ops::v8::op_v8_transfer_array_buffer, + ops::v8::op_v8_transfer_array_buffer_de, + ops::v8::op_v8_write_double, + ops::v8::op_v8_write_header, + ops::v8::op_v8_write_raw_bytes, + ops::v8::op_v8_write_uint32, + ops::v8::op_v8_write_uint64, + ops::v8::op_v8_write_value, ops::vm::op_vm_create_script, ops::vm::op_vm_create_context, ops::vm::op_vm_script_run_in_context, diff --git a/ext/node/ops/v8.rs b/ext/node/ops/v8.rs index ebcf6b080..8813d2e18 100644 --- a/ext/node/ops/v8.rs +++ b/ext/node/ops/v8.rs @@ -1,6 +1,15 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; +use deno_core::FastString; +use deno_core::GarbageCollected; +use deno_core::ToJsBuffer; +use std::ptr::NonNull; +use v8::ValueDeserializerHelper; +use v8::ValueSerializerHelper; #[op2(fast)] pub fn op_v8_cached_data_version_tag() -> u32 { @@ -30,3 +39,355 @@ pub fn op_v8_get_heap_statistics( buffer[12] = stats.used_global_handles_size() as f64; buffer[13] = stats.external_memory() as f64; } + +pub struct Serializer<'a> { + inner: v8::ValueSerializer<'a>, +} + +pub struct SerializerDelegate { + obj: v8::Global<v8::Object>, +} + +impl<'a> v8::cppgc::GarbageCollected for Serializer<'a> { + fn trace(&self, _visitor: &v8::cppgc::Visitor) {} +} + +impl SerializerDelegate { + fn obj<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + ) -> v8::Local<'s, v8::Object> { + v8::Local::new(scope, &self.obj) + } +} + +impl v8::ValueSerializerImpl for SerializerDelegate { + fn get_shared_array_buffer_id<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + shared_array_buffer: v8::Local<'s, v8::SharedArrayBuffer>, + ) -> Option<u32> { + let obj = self.obj(scope); + let key = FastString::from_static("_getSharedArrayBufferId") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + if let Ok(fun) = v.try_cast::<v8::Function>() { + return fun + .call(scope, obj.into(), &[shared_array_buffer.into()]) + .and_then(|ret| ret.uint32_value(scope)); + } + } + None + } + fn has_custom_host_object(&self, _isolate: &mut v8::Isolate) -> bool { + false + } + fn throw_data_clone_error<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + message: v8::Local<'s, v8::String>, + ) { + let obj = self.obj(scope); + let key = FastString::from_static("_getDataCloneError") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + let fun = v + .try_cast::<v8::Function>() + .expect("_getDataCloneError should be a function"); + if let Some(error) = fun.call(scope, obj.into(), &[message.into()]) { + scope.throw_exception(error); + return; + } + } + let error = v8::Exception::type_error(scope, message); + scope.throw_exception(error); + } + + fn write_host_object<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + object: v8::Local<'s, v8::Object>, + _value_serializer: &dyn ValueSerializerHelper, + ) -> Option<bool> { + let obj = self.obj(scope); + let key = FastString::from_static("_writeHostObject") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + if let Ok(v) = v.try_cast::<v8::Function>() { + v.call(scope, obj.into(), &[object.into()])?; + return Some(true); + } + } + + None + } + + fn is_host_object<'s>( + &self, + _scope: &mut v8::HandleScope<'s>, + _object: v8::Local<'s, v8::Object>, + ) -> Option<bool> { + // should never be called because has_custom_host_object returns false + None + } +} + +#[op2] +#[cppgc] +pub fn op_v8_new_serializer( + scope: &mut v8::HandleScope, + obj: v8::Local<v8::Object>, +) -> Serializer<'static> { + let obj = v8::Global::new(scope, obj); + let inner = + v8::ValueSerializer::new(scope, Box::new(SerializerDelegate { obj })); + Serializer { inner } +} + +#[op2(fast)] +pub fn op_v8_set_treat_array_buffer_views_as_host_objects( + #[cppgc] ser: &Serializer, + value: bool, +) { + ser + .inner + .set_treat_array_buffer_views_as_host_objects(value); +} + +#[op2] +#[serde] +pub fn op_v8_release_buffer(#[cppgc] ser: &Serializer) -> ToJsBuffer { + ser.inner.release().into() +} + +#[op2(fast)] +pub fn op_v8_transfer_array_buffer( + #[cppgc] ser: &Serializer, + #[smi] id: u32, + array_buffer: v8::Local<v8::ArrayBuffer>, +) { + ser.inner.transfer_array_buffer(id, array_buffer); +} + +#[op2(fast)] +pub fn op_v8_write_double(#[cppgc] ser: &Serializer, double: f64) { + ser.inner.write_double(double); +} + +#[op2(fast)] +pub fn op_v8_write_header(#[cppgc] ser: &Serializer) { + ser.inner.write_header(); +} + +#[op2] +pub fn op_v8_write_raw_bytes( + #[cppgc] ser: &Serializer, + #[anybuffer] source: &[u8], +) { + ser.inner.write_raw_bytes(source); +} + +#[op2(fast)] +pub fn op_v8_write_uint32(#[cppgc] ser: &Serializer, num: u32) { + ser.inner.write_uint32(num); +} + +#[op2(fast)] +pub fn op_v8_write_uint64(#[cppgc] ser: &Serializer, hi: u32, lo: u32) { + let num = ((hi as u64) << 32) | (lo as u64); + ser.inner.write_uint64(num); +} + +#[op2(nofast, reentrant)] +pub fn op_v8_write_value( + scope: &mut v8::HandleScope, + #[cppgc] ser: &Serializer, + value: v8::Local<v8::Value>, +) -> Result<(), AnyError> { + let context = scope.get_current_context(); + ser.inner.write_value(context, value); + Ok(()) +} + +struct DeserBuffer { + ptr: Option<NonNull<u8>>, + // Hold onto backing store to keep the underlying buffer + // alive while we hold a reference to it. + _backing_store: v8::SharedRef<v8::BackingStore>, +} + +pub struct Deserializer<'a> { + buf: DeserBuffer, + inner: v8::ValueDeserializer<'a>, +} + +impl<'a> deno_core::GarbageCollected for Deserializer<'a> {} + +pub struct DeserializerDelegate { + obj: v8::Global<v8::Object>, +} + +impl GarbageCollected for DeserializerDelegate { + fn trace(&self, _visitor: &v8::cppgc::Visitor) {} +} + +impl v8::ValueDeserializerImpl for DeserializerDelegate { + fn read_host_object<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + _value_deserializer: &dyn v8::ValueDeserializerHelper, + ) -> Option<v8::Local<'s, v8::Object>> { + let obj = v8::Local::new(scope, &self.obj); + let key = FastString::from_static("_readHostObject") + .v8_string(scope) + .into(); + let scope = &mut v8::AllowJavascriptExecutionScope::new(scope); + if let Some(v) = obj.get(scope, key) { + if let Ok(v) = v.try_cast::<v8::Function>() { + let result = v.call(scope, obj.into(), &[])?; + match result.try_cast() { + Ok(res) => return Some(res), + Err(_) => { + let msg = + FastString::from_static("readHostObject must return an object") + .v8_string(scope); + let error = v8::Exception::type_error(scope, msg); + scope.throw_exception(error); + return None; + } + } + } + } + None + } +} + +#[op2] +#[cppgc] +pub fn op_v8_new_deserializer( + scope: &mut v8::HandleScope, + obj: v8::Local<v8::Object>, + buffer: v8::Local<v8::ArrayBufferView>, +) -> Result<Deserializer<'static>, AnyError> { + let offset = buffer.byte_offset(); + let len = buffer.byte_length(); + let backing_store = buffer.get_backing_store().ok_or_else(|| { + generic_error("deserialization buffer has no backing store") + })?; + let (buf_slice, buf_ptr) = if let Some(data) = backing_store.data() { + // SAFETY: the offset is valid for the underlying buffer because we're getting it directly from v8 + let data_ptr = unsafe { data.as_ptr().cast::<u8>().add(offset) }; + ( + // SAFETY: the len is valid, from v8, and the data_ptr is valid (as above) + unsafe { std::slice::from_raw_parts(data_ptr.cast_const().cast(), len) }, + Some(data.cast()), + ) + } else { + (&[] as &[u8], None::<NonNull<u8>>) + }; + let obj = v8::Global::new(scope, obj); + let inner = v8::ValueDeserializer::new( + scope, + Box::new(DeserializerDelegate { obj }), + buf_slice, + ); + Ok(Deserializer { + inner, + buf: DeserBuffer { + _backing_store: backing_store, + ptr: buf_ptr, + }, + }) +} + +#[op2(fast)] +pub fn op_v8_transfer_array_buffer_de( + #[cppgc] deser: &Deserializer, + #[smi] id: u32, + array_buffer: v8::Local<v8::ArrayBuffer>, +) { + // TODO(nathanwhit): also need binding for TransferSharedArrayBuffer, then call that if + // array_buffer is shared + deser.inner.transfer_array_buffer(id, array_buffer); +} + +#[op2(fast)] +pub fn op_v8_read_double( + #[cppgc] deser: &Deserializer, +) -> Result<f64, AnyError> { + let mut double = 0f64; + if !deser.inner.read_double(&mut double) { + return Err(type_error("ReadDouble() failed")); + } + Ok(double) +} + +#[op2(nofast)] +pub fn op_v8_read_header( + scope: &mut v8::HandleScope, + #[cppgc] deser: &Deserializer, +) -> bool { + let context = scope.get_current_context(); + let res = deser.inner.read_header(context); + res.unwrap_or_default() +} + +#[op2(fast)] +#[number] +pub fn op_v8_read_raw_bytes( + #[cppgc] deser: &Deserializer, + #[number] length: usize, +) -> usize { + let Some(buf_ptr) = deser.buf.ptr else { + return 0; + }; + if let Some(buf) = deser.inner.read_raw_bytes(length) { + let ptr = buf.as_ptr(); + (ptr as usize) - (buf_ptr.as_ptr() as usize) + } else { + 0 + } +} + +#[op2(fast)] +pub fn op_v8_read_uint32( + #[cppgc] deser: &Deserializer, +) -> Result<u32, AnyError> { + let mut value = 0; + if !deser.inner.read_uint32(&mut value) { + return Err(type_error("ReadUint32() failed")); + } + + Ok(value) +} + +#[op2] +#[serde] +pub fn op_v8_read_uint64( + #[cppgc] deser: &Deserializer, +) -> Result<(u32, u32), AnyError> { + let mut val = 0; + if !deser.inner.read_uint64(&mut val) { + return Err(type_error("ReadUint64() failed")); + } + + Ok(((val >> 32) as u32, val as u32)) +} + +#[op2(fast)] +pub fn op_v8_get_wire_format_version(#[cppgc] deser: &Deserializer) -> u32 { + deser.inner.get_wire_format_version() +} + +#[op2(reentrant)] +pub fn op_v8_read_value<'s>( + scope: &mut v8::HandleScope<'s>, + #[cppgc] deser: &Deserializer, +) -> v8::Local<'s, v8::Value> { + let context = scope.get_current_context(); + let val = deser.inner.read_value(context); + val.unwrap_or_else(|| v8::null(scope).into()) +} diff --git a/ext/node/polyfills/v8.ts b/ext/node/polyfills/v8.ts index f06227cd5..5849f3ccc 100644 --- a/ext/node/polyfills/v8.ts +++ b/ext/node/polyfills/v8.ts @@ -6,15 +6,36 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { core } from "ext:core/mod.js"; +import { primordials } from "ext:core/mod.js"; +const { ObjectPrototypeToString } = primordials; import { op_v8_cached_data_version_tag, op_v8_get_heap_statistics, + op_v8_get_wire_format_version, + op_v8_new_deserializer, + op_v8_new_serializer, + op_v8_read_double, + op_v8_read_header, + op_v8_read_raw_bytes, + op_v8_read_uint32, + op_v8_read_uint64, + op_v8_read_value, + op_v8_release_buffer, + op_v8_set_treat_array_buffer_views_as_host_objects, + op_v8_transfer_array_buffer, + op_v8_transfer_array_buffer_de, + op_v8_write_double, + op_v8_write_header, + op_v8_write_raw_bytes, + op_v8_write_uint32, + op_v8_write_uint64, + op_v8_write_value, } from "ext:core/ops"; import { Buffer } from "node:buffer"; -import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; +import { notImplemented } from "ext:deno_node/_utils.ts"; +import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; export function cachedDataVersionTag() { return op_v8_cached_data_version_tag(); @@ -71,65 +92,225 @@ export function takeCoverage() { export function writeHeapSnapshot() { notImplemented("v8.writeHeapSnapshot"); } -export function serialize(value) { - return Buffer.from(core.serialize(value)); +// deno-lint-ignore no-explicit-any +export function serialize(value: any) { + const ser = new DefaultSerializer(); + ser.writeHeader(); + ser.writeValue(value); + return ser.releaseBuffer(); } -export function deserialize(data) { - return core.deserialize(data); +export function deserialize(buffer: Buffer | ArrayBufferView | DataView) { + if (!isArrayBufferView(buffer)) { + throw new TypeError( + "buffer must be a TypedArray or a DataView", + ); + } + const der = new DefaultDeserializer(buffer); + der.readHeader(); + return der.readValue(); } + +const kHandle = Symbol("kHandle"); + export class Serializer { + [kHandle]: object; constructor() { - warnNotImplemented("v8.Serializer.prototype.constructor"); + this[kHandle] = op_v8_new_serializer(this); + } + + _setTreatArrayBufferViewsAsHostObjects(value: boolean): void { + op_v8_set_treat_array_buffer_views_as_host_objects(this[kHandle], value); } releaseBuffer(): Buffer { - warnNotImplemented("v8.DefaultSerializer.prototype.releaseBuffer"); - return Buffer.from(""); + return Buffer.from(op_v8_release_buffer(this[kHandle])); } transferArrayBuffer(_id: number, _arrayBuffer: ArrayBuffer): void { - warnNotImplemented("v8.DefaultSerializer.prototype.transferArrayBuffer"); + op_v8_transfer_array_buffer(this[kHandle], _id, _arrayBuffer); } - writeDouble(_value: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeDouble"); + writeDouble(value: number): void { + op_v8_write_double(this[kHandle], value); } writeHeader(): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeHeader"); + op_v8_write_header(this[kHandle]); } - writeRawBytes(_value: ArrayBufferView): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeRawBytes"); + writeRawBytes(source: ArrayBufferView): void { + if (!isArrayBufferView(source)) { + throw new TypeError( + "source must be a TypedArray or a DataView", + ); + } + op_v8_write_raw_bytes(this[kHandle], source); } - writeUint32(_value: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeUint32"); + writeUint32(value: number): void { + op_v8_write_uint32(this[kHandle], value); } - writeUint64(_hi: number, _lo: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeUint64"); + writeUint64(hi: number, lo: number): void { + op_v8_write_uint64(this[kHandle], hi, lo); } // deno-lint-ignore no-explicit-any - writeValue(_value: any): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeValue"); + writeValue(value: any): void { + op_v8_write_value(this[kHandle], value); } + + _getDataCloneError = Error; } + export class Deserializer { - constructor() { - notImplemented("v8.Deserializer.prototype.constructor"); + buffer: ArrayBufferView; + [kHandle]: object; + constructor(buffer: ArrayBufferView) { + if (!isArrayBufferView(buffer)) { + throw new TypeError( + "buffer must be a TypedArray or a DataView", + ); + } + this.buffer = buffer; + this[kHandle] = op_v8_new_deserializer(this, buffer); + } + readRawBytes(length: number): Buffer { + const offset = this._readRawBytes(length); + return Buffer.from( + this.buffer.buffer, + this.buffer.byteOffset + offset, + length, + ); + } + _readRawBytes(length: number): number { + return op_v8_read_raw_bytes(this[kHandle], length); + } + getWireFormatVersion(): number { + return op_v8_get_wire_format_version(this[kHandle]); + } + readDouble(): number { + return op_v8_read_double(this[kHandle]); + } + readHeader(): boolean { + return op_v8_read_header(this[kHandle]); + } + + readUint32(): number { + return op_v8_read_uint32(this[kHandle]); } + readUint64(): [hi: number, lo: number] { + return op_v8_read_uint64(this[kHandle]); + } + readValue(): unknown { + return op_v8_read_value(this[kHandle]); + } + transferArrayBuffer( + id: number, + arrayBuffer: ArrayBuffer | SharedArrayBuffer, + ): void { + return op_v8_transfer_array_buffer_de(this[kHandle], id, arrayBuffer); + } +} +function arrayBufferViewTypeToIndex(abView: ArrayBufferView) { + const type = ObjectPrototypeToString(abView); + if (type === "[object Int8Array]") return 0; + if (type === "[object Uint8Array]") return 1; + if (type === "[object Uint8ClampedArray]") return 2; + if (type === "[object Int16Array]") return 3; + if (type === "[object Uint16Array]") return 4; + if (type === "[object Int32Array]") return 5; + if (type === "[object Uint32Array]") return 6; + if (type === "[object Float32Array]") return 7; + if (type === "[object Float64Array]") return 8; + if (type === "[object DataView]") return 9; + // Index 10 is FastBuffer. + if (type === "[object BigInt64Array]") return 11; + if (type === "[object BigUint64Array]") return 12; + return -1; } export class DefaultSerializer extends Serializer { constructor() { - warnNotImplemented("v8.DefaultSerializer.prototype.constructor"); super(); + this._setTreatArrayBufferViewsAsHostObjects(true); + } + + // deno-lint-ignore no-explicit-any + _writeHostObject(abView: any) { + // Keep track of how to handle different ArrayBufferViews. The default + // Serializer for Node does not use the V8 methods for serializing those + // objects because Node's `Buffer` objects use pooled allocation in many + // cases, and their underlying `ArrayBuffer`s would show up in the + // serialization. Because a) those may contain sensitive data and the user + // may not be aware of that and b) they are often much larger than the + // `Buffer` itself, custom serialization is applied. + let i = 10; // FastBuffer + if (abView.constructor !== Buffer) { + i = arrayBufferViewTypeToIndex(abView); + if (i === -1) { + throw new this._getDataCloneError( + `Unserializable host object: ${abView}`, + ); + } + } + this.writeUint32(i); + this.writeUint32(abView.byteLength); + this.writeRawBytes( + new Uint8Array(abView.buffer, abView.byteOffset, abView.byteLength), + ); } } -export class DefaultDeserializer { - constructor() { - notImplemented("v8.DefaultDeserializer.prototype.constructor"); + +// deno-lint-ignore no-explicit-any +function arrayBufferViewIndexToType(index: number): any { + if (index === 0) return Int8Array; + if (index === 1) return Uint8Array; + if (index === 2) return Uint8ClampedArray; + if (index === 3) return Int16Array; + if (index === 4) return Uint16Array; + if (index === 5) return Int32Array; + if (index === 6) return Uint32Array; + if (index === 7) return Float32Array; + if (index === 8) return Float64Array; + if (index === 9) return DataView; + if (index === 10) return Buffer; + if (index === 11) return BigInt64Array; + if (index === 12) return BigUint64Array; + return undefined; +} + +export class DefaultDeserializer extends Deserializer { + constructor(buffer: ArrayBufferView) { + super(buffer); + } + + _readHostObject() { + const typeIndex = this.readUint32(); + const ctor = arrayBufferViewIndexToType(typeIndex); + const byteLength = this.readUint32(); + const byteOffset = this._readRawBytes(byteLength); + const BYTES_PER_ELEMENT = ctor?.BYTES_PER_ELEMENT ?? 1; + + const offset = this.buffer.byteOffset + byteOffset; + if (offset % BYTES_PER_ELEMENT === 0) { + return new ctor( + this.buffer.buffer, + offset, + byteLength / BYTES_PER_ELEMENT, + ); + } + // Copy to an aligned buffer first. + const bufferCopy = Buffer.allocUnsafe(byteLength); + Buffer.from( + this.buffer.buffer, + byteOffset, + byteLength, + ).copy(bufferCopy); + return new ctor( + bufferCopy.buffer, + bufferCopy.byteOffset, + byteLength / BYTES_PER_ELEMENT, + ); } } export const promiseHooks = { |