From 3d05b97eeb58873633ec41e03bb17e886c35b3d4 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 12 Oct 2026 11:36:32 +0000 Subject: [PATCH] lib: avoid using __proto__: null in AbortController/AbortSignal Using `__proto__: null` on objects will properties immediately puts them into the slower DictionaryProperties mode. Using a `__proto__: { __proto: null }, ...` pattern, however, keeps the objects in FastProperties mode. Technically this is an observable change since calling `Object.getPrototoTypeOf(...)` some of these will no longer return `null`. So a decision needs to be made about whether these types of changes are `semver-major` or not. Signed-off-by: James M Snell --- lib/internal/abort_controller.js | 39 ++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js index 55be0b6f3669..9dc91a3ea51a 100644 --- a/lib/internal/abort_controller.js +++ b/lib/internal/abort_controller.js @@ -8,6 +8,7 @@ const { ObjectAssign, ObjectDefineProperties, ObjectDefineProperty, + ObjectFreeze, ObjectSetPrototypeOf, PromiseResolve, PromiseWithResolvers, @@ -73,6 +74,15 @@ const { let _MessageChannel; +// An object literal with __proto__: null and properties will be +// created as slow Dictionary properties. An object literal with +// __proto__: null and no properties is fast mode. An object +// literal with { __proto__: kNullProtoObject, ... } and +// properties will be fast mode. It's a V8 optimization quirk. +// To ensure that object literals are created in fast mode, +// use __proto__: kNullProtoObject. +const kNullProtoObject = ObjectFreeze({ __proto__: null }); + const kDontThrowSymbol = Symbol('kDontThrowSymbol'); // Loading the MessageChannel and markTransferable have to be done lazily @@ -295,7 +305,7 @@ class AbortSignal extends EventTarget { */ static abort( reason = new DOMException('This operation was aborted', 'AbortError')) { - return new AbortSignal(kDontThrowSymbol, { aborted: true, reason }); + return new AbortSignal(kDontThrowSymbol, { __proto__: kNullProtoObject, aborted: true, reason }); } /** @@ -320,10 +330,10 @@ class AbortSignal extends EventTarget { static any(signals) { const signalsArray = converters['sequence']( signals, - { __proto__: null, context: 'signals' }, + { __proto__: kNullProtoObject, context: 'signals' }, ); - const resultSignal = new AbortSignal(kDontThrowSymbol, { composite: true }); + const resultSignal = new AbortSignal(kDontThrowSymbol, { __proto__: kNullProtoObject, composite: true }); if (!signalsArray.length) { return resultSignal; } @@ -423,7 +433,7 @@ class AbortSignal extends EventTarget { if (aborted) { const reason = this.reason; return { - data: { aborted, reason }, + data: { __proto__: kNullProtoObject, aborted, reason }, deserializeInfo: 'internal/abort_controller:ClonedAbortSignal', }; } @@ -434,10 +444,10 @@ class AbortSignal extends EventTarget { this.addEventListener('abort', () => { port1.postMessage(this.reason); port1.close(); - }, { once: true }); + }, { __proto__: kNullProtoObject, once: true }); return { - data: { port: port2 }, + data: { __proto__: kNullProtoObject, port: port2 }, deserializeInfo: 'internal/abort_controller:ClonedAbortSignal', }; } @@ -477,7 +487,7 @@ class AbortSignal extends EventTarget { converters['sequence'] = createSequenceConverter(converters.AbortSignal); function ClonedAbortSignal() { - return new AbortSignal(kDontThrowSymbol, { transferable: true }); + return new AbortSignal(kDontThrowSymbol, { __proto__: kNullProtoObject, transferable: true }); } ClonedAbortSignal.prototype[kDeserialize] = () => {}; @@ -485,8 +495,12 @@ ObjectDefineProperties(AbortSignal.prototype, { aborted: kEnumerableProperty, }); +// TODO(@jasnell): the kNullProtoObject is a null-prototype object but it is +// not recognized as such by the esline rule, which will need to be updated +// to recognize this pattern. +// eslint-disable-next-line node-core/avoid-prototype-pollution ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, { - __proto__: null, + __proto__: kNullProtoObject, writable: false, enumerable: false, configurable: true, @@ -588,7 +602,7 @@ class AbortController { static [kMakeTransferable]() { const controller = new AbortController(); - controller.#signal = new AbortSignal(kDontThrowSymbol, { transferable: true }); + controller.#signal = new AbortSignal(kDontThrowSymbol, { __proto__: kNullProtoObject, transferable: true }); return controller; } } @@ -619,12 +633,12 @@ function transferableAbortController() { * @returns {Promise} */ async function aborted(signal, resource) { - converters.AbortSignal(signal, { __proto__: null, context: 'signal' }); + converters.AbortSignal(signal, { __proto__: kNullProtoObject, context: 'signal' }); validateObject(resource, 'resource', kValidateObjectAllowObjects); if (signal.aborted) return PromiseResolve(); const abortPromise = PromiseWithResolvers(); - const opts = { __proto__: null, [kWeakHandler]: resource, once: true, [kResistStopPropagation]: true }; + const opts = { __proto__: kNullProtoObject, [kWeakHandler]: resource, once: true, [kResistStopPropagation]: true }; signal.addEventListener('abort', abortPromise.resolve, opts); return abortPromise.promise; } @@ -634,8 +648,9 @@ ObjectDefineProperties(AbortController.prototype, { abort: kEnumerableProperty, }); +// eslint-disable-next-line node-core/avoid-prototype-pollution ObjectDefineProperty(AbortController.prototype, SymbolToStringTag, { - __proto__: null, + __proto__: kNullProtoObject, writable: false, enumerable: false, configurable: true,