Repository: denoland/deno
Pull request: fix(ext/node): Rewrite `node:v8` serialize/deserialize

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`.



Changed files:
filename: ext/node/lib.rs
patch: @@ -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,

filename: ext/node/ops/v8.rs
patch: @@ -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())
+}

filename: ext/node/polyfills/v8.ts
patch: @@ -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 = {

filename: tests/node_compat/config.jsonc
patch: @@ -104,6 +104,7 @@
       "test-util-promisify.js",
       "test-util-types.js",
       "test-util.js",
+      "test-v8-serdes.js",
       "test-webcrypto-sign-verify.js",
       "test-whatwg-url-properties.js",
       // needs replace ".on" => ".addEventListener" in L29

filename: tests/node_compat/test/parallel/test-v8-serdes.js
patch: @@ -0,0 +1,285 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 18.12.1
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+// Flags: --expose-internals
+
+'use strict';
+
+const common = require('../common');
+const { internalBinding } = require('internal/test/binding');
+const assert = require('assert');
+const v8 = require('v8');
+const os = require('os');
+
+const circular = {};
+circular.circular = circular;
+
+const objects = [
+  { foo: 'bar' },
+  { bar: 'baz' },
+  new Int8Array([1, 2, 3, 4]),
+  new Uint8Array([1, 2, 3, 4]),
+  new Int16Array([1, 2, 3, 4]),
+  new Uint16Array([1, 2, 3, 4]),
+  new Int32Array([1, 2, 3, 4]),
+  new Uint32Array([1, 2, 3, 4]),
+  new Float32Array([1, 2, 3, 4]),
+  new Float64Array([1, 2, 3, 4]),
+  new DataView(new ArrayBuffer(42)),
+  Buffer.from([1, 2, 3, 4]),
+  new BigInt64Array([42n]),
+  new BigUint64Array([42n]),
+  undefined,
+  null,
+  42,
+  circular,
+];
+
+// TODO(nathanwhit): we don't have any exposed host objects, so these parts don't work
+// const hostObject = new (internalBinding('js_stream').JSStream)();
+
+{
+  const ser = new v8.DefaultSerializer();
+  ser.writeHeader();
+  for (const obj of objects) {
+    ser.writeValue(obj);
+  }
+
+  const des = new v8.DefaultDeserializer(ser.releaseBuffer());
+  des.readHeader();
+
+  for (const obj of objects) {
+    assert.deepStrictEqual(des.readValue(), obj);
+  }
+}
+
+{
+  for (const obj of objects) {
+    assert.deepStrictEqual(v8.deserialize(v8.serialize(obj)), obj);
+  }
+}
+
+{
+  const ser = new v8.DefaultSerializer();
+  ser._getDataCloneError = common.mustCall((message) => {
+    assert.strictEqual(message, '#<Object> could not be cloned.');
+    return new Error('foobar');
+  });
+
+  ser.writeHeader();
+
+  assert.throws(() => {
+    ser.writeValue(new Proxy({}, {}));
+  }, /foobar/);
+}
+
+// TODO(nathanwhit): we don't have any exposed host objects, so these parts don't work
+// {
+//   const ser = new v8.DefaultSerializer();
+//   ser._writeHostObject = common.mustCall((object) => {
+//     assert.strictEqual(object, hostObject);
+//     const buf = Buffer.from('hostObjectTag');
+
+//     ser.writeUint32(buf.length);
+//     ser.writeRawBytes(buf);
+
+//     ser.writeUint64(1, 2);
+//     ser.writeDouble(-0.25);
+//   });
+
+//   ser.writeHeader();
+//   ser.writeValue({ val: hostObject });
+
+//   const des = new v8.DefaultDeserializer(ser.releaseBuffer());
+//   des._readHostObject = common.mustCall(() => {
+//     const length = des.readUint32();
+//     const buf = des.readRawBytes(length);
+
+//     assert.strictEqual(buf.toString(), 'hostObjectTag');
+
+//     assert.deepStrictEqual(des.readUint64(), [1, 2]);
+//     assert.strictEqual(des.readDouble(), -0.25);
+//     return hostObject;
+//   });
+
+//   des.readHeader();
+
+//   assert.strictEqual(des.readValue().val, hostObject);
+// }
+
+// This test ensures that `v8.Serializer.writeRawBytes()` support
+// `TypedArray` and `DataView`.
+// {
+//   const text = 'hostObjectTag';
+//   const data = Buffer.from(text);
+//   const arrayBufferViews = common.getArrayBufferViews(data);
+
+//   // `buf` is one of `TypedArray` or `DataView`.
+//   function testWriteRawBytes(buf) {
+//     let writeHostObjectCalled = false;
+//     const ser = new v8.DefaultSerializer();
+
+//     ser._writeHostObject = common.mustCall((object) => {
+//       writeHostObjectCalled = true;
+//       ser.writeUint32(buf.byteLength);
+//       ser.writeRawBytes(buf);
+//     });
+
+//     ser.writeHeader();
+//     ser.writeValue({ val: hostObject });
+
+//     const des = new v8.DefaultDeserializer(ser.releaseBuffer());
+//     des._readHostObject = common.mustCall(() => {
+//       assert.strictEqual(writeHostObjectCalled, true);
+//       const length = des.readUint32();
+//       const buf = des.readRawBytes(length);
+//       assert.strictEqual(buf.toString(), text);
+
+//       return hostObject;
+//     });
+
+//     des.readHeader();
+
+//     assert.strictEqual(des.readValue().val, hostObject);
+//   }
+
+//   arrayBufferViews.forEach((buf) => {
+//     testWriteRawBytes(buf);
+//   });
+// }
+
+// {
+//   const ser = new v8.DefaultSerializer();
+//   ser._writeHostObject = common.mustCall((object) => {
+//     throw new Error('foobar');
+//   });
+
+//   ser.writeHeader();
+//   assert.throws(() => {
+//     ser.writeValue({ val: hostObject });
+//   }, /foobar/);
+// }
+
+// {
+//   assert.throws(() => v8.serialize(hostObject), {
+//     constructor: Error,
+//     message: 'Unserializable host object: JSStream {}'
+//   });
+// }
+
+{
+  // Test that an old serialized value can still be deserialized.
+  const buf = Buffer.from('ff0d6f2203666f6f5e007b01', 'hex');
+
+  const des = new v8.DefaultDeserializer(buf);
+  des.readHeader();
+  assert.strictEqual(des.getWireFormatVersion(), 0x0d);
+
+  const value = des.readValue();
+  assert.strictEqual(value, value.foo);
+}
+
+{
+  const message = `New serialization format.
+
+    This test is expected to fail when V8 changes its serialization format.
+    When that happens, the "desStr" variable must be updated to the new value
+    and the change should be mentioned in the release notes, as it is semver-major.
+
+    Consider opening an issue as a heads up at https://github.com/nodejs/node/issues/new
+  `;
+
+  const desStr = 'ff0f6f2203666f6f5e007b01';
+
+  const desBuf = Buffer.from(desStr, 'hex');
+  const des = new v8.DefaultDeserializer(desBuf);
+  des.readHeader();
+  const value = des.readValue();
+
+  const ser = new v8.DefaultSerializer();
+  ser.writeHeader();
+  ser.writeValue(value);
+
+  const serBuf = ser.releaseBuffer();
+  const serStr = serBuf.toString('hex');
+  assert.deepStrictEqual(serStr, desStr, message);
+}
+
+{
+  // Unaligned Uint16Array read, with padding in the underlying array buffer.
+  let buf = Buffer.alloc(32 + 9);
+  buf.write('ff0d5c0404addeefbe', 32, 'hex');
+  buf = buf.slice(32);
+
+  const expectedResult = os.endianness() === 'LE' ?
+    new Uint16Array([0xdead, 0xbeef]) : new Uint16Array([0xadde, 0xefbe]);
+
+  assert.deepStrictEqual(v8.deserialize(buf), expectedResult);
+}
+
+{
+  assert.throws(() => v8.Serializer(), {
+    constructor: TypeError,
+    message: "Class constructor Serializer cannot be invoked without 'new'",
+    // code: 'ERR_CONSTRUCT_CALL_REQUIRED'
+  });
+  assert.throws(() => v8.Deserializer(), {
+    constructor: TypeError,
+    message: "Class constructor Deserializer cannot be invoked without 'new'",
+    // code: 'ERR_CONSTRUCT_CALL_REQUIRED'
+  });
+}
+
+
+// `v8.deserialize()` and `new v8.Deserializer()` should support both
+// `TypedArray` and `DataView`.
+{
+  for (const obj of objects) {
+    const buf = v8.serialize(obj);
+
+    for (const arrayBufferView of common.getArrayBufferViews(buf)) {
+      assert.deepStrictEqual(v8.deserialize(arrayBufferView), obj);
+    }
+
+    for (const arrayBufferView of common.getArrayBufferViews(buf)) {
+      const deserializer = new v8.DefaultDeserializer(arrayBufferView);
+      deserializer.readHeader();
+      const value = deserializer.readValue();
+      assert.deepStrictEqual(value, obj);
+
+      const serializer = new v8.DefaultSerializer();
+      serializer.writeHeader();
+      serializer.writeValue(value);
+      assert.deepStrictEqual(buf, serializer.releaseBuffer());
+    }
+  }
+}
+
+{
+  const INVALID_SOURCE = 'INVALID_SOURCE_TYPE';
+  const serializer = new v8.Serializer();
+  serializer.writeHeader();
+  assert.throws(
+    () => serializer.writeRawBytes(INVALID_SOURCE),
+    /^TypeError: source must be a TypedArray or a DataView$/,
+  );
+  assert.throws(
+    () => v8.deserialize(INVALID_SOURCE),
+    /^TypeError: buffer must be a TypedArray or a DataView$/,
+  );
+  assert.throws(
+    () => new v8.Deserializer(INVALID_SOURCE),
+    /^TypeError: buffer must be a TypedArray or a DataView$/,
+  );
+}
+
+{
+  // Regression test for https://github.com/nodejs/node/issues/37978
+  assert.throws(() => {
+    new v8.Deserializer(new v8.Serializer().releaseBuffer()).readDouble();
+  }, /ReadDouble\(\) failed/);
+}