From 278acb46a89593f6e1f19bf31f801d0d3fd88664 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 11 Sep 2026 07:26:10 +0000 Subject: [PATCH] node:util: build aborted()'s abort listener without Function.prototype.bind aborted() created its listener with onAbortedCallback.bind(undefined, promise). `bind` is looked up on Function.prototype, so a replaced bind received the internal onAbortedCallback as its receiver. Create the listener with a small closure instead, which does no user-visible lookups and still keeps `resource` out of the listener's scope. --- src/js/node/util.ts | 16 ++++++++-------- test/js/node/util/test-aborted.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 5b545172dbe9..ef30cf174d8f 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -628,6 +628,12 @@ function onAbortedCallback(promise: Promise) { $resolvePromiseWithFirstResolvingFunctionCallCheck(promise, undefined); } +// Its own function so the listener's scope holds `promise` and not aborted()'s `resource`. Not +// onAbortedCallback.bind(): that looks up `bind` on Function.prototype, which user code can replace. +function createAbortedListener(promise: Promise) { + return () => onAbortedCallback(promise); +} + function aborted(signal: AbortSignal, resource: object) { if (!$isObject(signal) || !(signal instanceof AbortSignal)) { throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); @@ -642,14 +648,8 @@ function aborted(signal: AbortSignal, resource: object) { } const promise = $newPromise(); - const listener = onAbortedCallback.bind(undefined, promise); - signal.addEventListener( - "abort", - // Do not leak the current scope into the listener. - // Instead, create a new function. - listener, - resistStopPropagation({ __proto__: null, once: true }), - ); + const listener = createAbortedListener(promise); + signal.addEventListener("abort", listener, resistStopPropagation({ __proto__: null, once: true })); if (!lazyAbortedRegistry) { lazyAbortedRegistry = new FinalizationRegistry(({ ref, listener }) => { diff --git a/test/js/node/util/test-aborted.test.ts b/test/js/node/util/test-aborted.test.ts index fc6efb27087f..0f1e25b50109 100644 --- a/test/js/node/util/test-aborted.test.ts +++ b/test/js/node/util/test-aborted.test.ts @@ -106,3 +106,18 @@ test("aborted resolves every waiter on the same signal with undefined", async () expect(await Promise.all([first, second])).toEqual([undefined, undefined]); expect(getEventListeners(ac.signal, "abort")).toHaveLength(0); }); + +test("aborted does not hand its internal settle function to a patched Function.prototype.bind", () => { + const originalBind = Function.prototype.bind; + const receivers: Function[] = []; + Function.prototype.bind = function (this: Function, ...args: any[]) { + receivers.push(this); + return originalBind.apply(this, args as [any, ...any[]]); + }; + try { + aborted(new AbortController().signal, {}); + } finally { + Function.prototype.bind = originalBind; + } + expect(receivers).toEqual([]); +});