From 1ab238207d84196479428494d52fc922833e8e5b Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Sat, 12 Oct 2024 08:26:54 +1030 Subject: [PATCH] web: mark as uncloneable when possible (#3709) * web: mark as uncloneable when possible This tells node that the marked instances from undici are not cloneable, so that attempts to cloning those throw `DataCloneError`. * test: add test cases for platform objects uncloneable * fix: move to webidl * fix: move it under webidl * fixup: rename it to markAsUncloneable * fix: mark more web instances uncloneable * fixup --------- Co-authored-by: Khafra --- lib/web/cache/cache.js | 1 + lib/web/cache/cachestorage.js | 2 ++ lib/web/eventsource/eventsource.js | 2 ++ lib/web/fetch/formdata.js | 2 ++ lib/web/fetch/headers.js | 2 ++ lib/web/fetch/request.js | 2 ++ lib/web/fetch/response.js | 2 ++ lib/web/fetch/webidl.js | 3 +++ lib/web/websocket/events.js | 4 ++++ lib/web/websocket/websocket.js | 2 ++ test/node-platform-objects.js | 33 ++++++++++++++++++++++++++++++ types/webidl.d.ts | 6 ++++++ 12 files changed, 61 insertions(+) create mode 100644 test/node-platform-objects.js diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js index 50c0eeaf0bc..435891dba8f 100644 --- a/lib/web/cache/cache.js +++ b/lib/web/cache/cache.js @@ -36,6 +36,7 @@ class Cache { webidl.illegalConstructor() } + webidl.util.markAsUncloneable(this) this.#relevantRequestResponseList = arguments[1] } diff --git a/lib/web/cache/cachestorage.js b/lib/web/cache/cachestorage.js index 3e35200b626..3b1604c872d 100644 --- a/lib/web/cache/cachestorage.js +++ b/lib/web/cache/cachestorage.js @@ -16,6 +16,8 @@ class CacheStorage { if (arguments[0] !== kConstruct) { webidl.illegalConstructor() } + + webidl.util.markAsUncloneable(this) } async match (request, options = {}) { diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index 257b9573ef9..02f4d47d006 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -109,6 +109,8 @@ class EventSource extends EventTarget { // 1. Let ev be a new EventSource object. super() + webidl.util.markAsUncloneable(this) + const prefix = 'EventSource constructor' webidl.argumentLengthCheck(arguments, 1, prefix) diff --git a/lib/web/fetch/formdata.js b/lib/web/fetch/formdata.js index c115528f1dd..8020a26e116 100644 --- a/lib/web/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -14,6 +14,8 @@ class FormData { #state = [] constructor (form) { + webidl.util.markAsUncloneable(this) + if (form !== undefined) { throw webidl.errors.conversionFailed({ prefix: 'FormData constructor', diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index 44b1f52bb3f..cc4d138140b 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -436,6 +436,8 @@ class Headers { * @returns */ constructor (init = undefined) { + webidl.util.markAsUncloneable(this) + if (init === kConstruct) { return } diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index d7520ef798b..97fea22cdbd 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -92,6 +92,8 @@ class Request { // https://fetch.spec.whatwg.org/#dom-request constructor (input, init = undefined) { + webidl.util.markAsUncloneable(this) + if (input === kConstruct) { return } diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index 3e5590a46cf..be82c25c4c5 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -112,6 +112,8 @@ class Response { // https://fetch.spec.whatwg.org/#dom-response constructor (body = null, init = undefined) { + webidl.util.markAsUncloneable(this) + if (body === kConstruct) { return } diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index 7e584677892..23ba8f57df2 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -1,6 +1,7 @@ 'use strict' const { types, inspect } = require('node:util') +const { markAsUncloneable } = require('node:worker_threads') const { toUSVString } = require('../../core/util') const UNDEFINED = 1 @@ -131,6 +132,8 @@ webidl.util.TypeValueToString = function (o) { } } +webidl.util.markAsUncloneable = markAsUncloneable || (() => {}) + // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { let upperBound diff --git a/lib/web/websocket/events.js b/lib/web/websocket/events.js index d61f6e9634a..343227997e8 100644 --- a/lib/web/websocket/events.js +++ b/lib/web/websocket/events.js @@ -13,6 +13,7 @@ class MessageEvent extends Event { constructor (type, eventInitDict = {}) { if (type === kConstruct) { super(arguments[1], arguments[2]) + webidl.util.markAsUncloneable(this) return } @@ -25,6 +26,7 @@ class MessageEvent extends Event { super(type, eventInitDict) this.#eventInit = eventInitDict + webidl.util.markAsUncloneable(this) } get data () { @@ -111,6 +113,7 @@ class CloseEvent extends Event { super(type, eventInitDict) this.#eventInit = eventInitDict + webidl.util.markAsUncloneable(this) } get wasClean () { @@ -141,6 +144,7 @@ class ErrorEvent extends Event { webidl.argumentLengthCheck(arguments, 1, prefix) super(type, eventInitDict) + webidl.util.markAsUncloneable(this) type = webidl.converters.DOMString(type, prefix, 'type') eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 85e83b2296d..63bf67c6f8e 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -100,6 +100,8 @@ class WebSocket extends EventTarget { constructor (url, protocols = []) { super() + webidl.util.markAsUncloneable(this) + const prefix = 'WebSocket constructor' webidl.argumentLengthCheck(arguments, 1, prefix) diff --git a/test/node-platform-objects.js b/test/node-platform-objects.js new file mode 100644 index 00000000000..19e2b01a507 --- /dev/null +++ b/test/node-platform-objects.js @@ -0,0 +1,33 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') +const { markAsUncloneable } = require('node:worker_threads') +const { Response, Request, FormData, Headers, ErrorEvent, MessageEvent, CloseEvent, EventSource, WebSocket } = require('..') +const { CacheStorage } = require('../lib/web/cache/cachestorage') +const { Cache } = require('../lib/web/cache/cache') +const { kConstruct } = require('../lib/core/symbols') + +test('unserializable web instances should be uncloneable if node exposes the api', (t) => { + if (markAsUncloneable !== undefined) { + t = tspl(t, { plan: 11 }) + const uncloneables = [ + { Uncloneable: Response, brand: 'Response' }, + { Uncloneable: Request, value: 'http://localhost', brand: 'Request' }, + { Uncloneable: FormData, brand: 'FormData' }, + { Uncloneable: MessageEvent, value: 'dummy event', brand: 'MessageEvent' }, + { Uncloneable: CloseEvent, value: 'dummy event', brand: 'CloseEvent' }, + { Uncloneable: ErrorEvent, value: 'dummy event', brand: 'ErrorEvent' }, + { Uncloneable: EventSource, value: 'http://localhost', brand: 'EventSource' }, + { Uncloneable: Headers, brand: 'Headers' }, + { Uncloneable: WebSocket, value: 'http://localhost', brand: 'WebSocket' }, + { Uncloneable: Cache, value: kConstruct, brand: 'Cache' }, + { Uncloneable: CacheStorage, value: kConstruct, brand: 'CacheStorage' } + ] + uncloneables.forEach((platformEntity) => { + t.throws(() => structuredClone(new platformEntity.Uncloneable(platformEntity.value)), + DOMException, + `Cloning ${platformEntity.brand} should throw DOMException`) + }) + } +}) diff --git a/types/webidl.d.ts b/types/webidl.d.ts index 10037eb5362..33b93439aae 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -85,6 +85,12 @@ interface WebidlUtil { Stringify (V: any): string MakeTypeAssertion (I: I): (arg: any) => arg is I + + /** + * Mark a value as uncloneable for Node.js. + * This is only effective in some newer Node.js versions. + */ + markAsUncloneable (V: any): void } interface WebidlConverters {