Commit 1d3b65d1014 for nodejs

commit 1d3b65d1014e61beef94f18e2dfe514e47c6f59d
Author: Darius <kxenk@protonmail.com>
Date:   Mon Oct 12 09:41:16 2026 +0300

    lib: fix AbortSignal.any() nested composite leak

    AbortSignal.any() copies the source WeakRefs of a composite input into
    the new composite, so nested composites share them. Those WeakRefs were
    also used as the unregister tokens for the registrations that prune a
    composite's sources once they are collected. When an inner composite
    was collected first, its cleanup therefore also unregistered the outer
    composite from the same sources. An observed outer composite then
    stayed in gcPersistentSignals, together with its listeners, even after
    all of its sources were gone and nothing could abort it anymore.

    Use each composite's own WeakRef as the unregister token instead, so
    collecting one composite only removes its own registrations.

    Fixes: https://github.com/nodejs/node/issues/66612
    Refs: https://github.com/nodejs/node/pull/62367
    Assisted-by: Claude Code
    Signed-off-by: journaltraces <kxenk@protonmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66617
    Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
    Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>

diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js
index 55be0b6f366..9722484a422 100644
--- a/lib/internal/abort_controller.js
+++ b/lib/internal/abort_controller.js
@@ -86,8 +86,8 @@ function lazyMessageChannel() {

 const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
 const dependantSignalsCleanupRegistry = new SafeFinalizationRegistry(
-  ({ sourceSignalRef, dependantSignalRef, sourceSignalsCleanupToken }) => {
-    sourceSignalsCleanupRegistry.unregister(sourceSignalsCleanupToken);
+  ({ sourceSignalRef, dependantSignalRef }) => {
+    sourceSignalsCleanupRegistry.unregister(dependantSignalRef);

     const sourceSignal = sourceSignalRef.deref();
     if (sourceSignal === undefined) {
@@ -180,12 +180,14 @@ function followCompositeSignal(signal) {
     dependantSignalsCleanupRegistry.register(signal, {
       sourceSignalRef: sourceSignalWeakRef,
       dependantSignalRef: resultSignalWeakRef,
-      sourceSignalsCleanupToken: sourceSignalWeakRef,
     });
+    // Nested composites share their sources' WeakRefs, so use this signal's
+    // own WeakRef as the unregister token. Otherwise collecting an inner
+    // composite would also unregister the outer composite from its sources.
     sourceSignalsCleanupRegistry.register(sourceSignal, {
       sourceSignalRef: sourceSignalWeakRef,
       composedSignalRef: resultSignalWeakRef,
-    }, sourceSignalWeakRef);
+    }, resultSignalWeakRef);
   }

   signal[kFollowing] = true;
diff --git a/test/parallel/test-abortsignal-drop-settled-signals.mjs b/test/parallel/test-abortsignal-drop-settled-signals.mjs
index d4c81a7165d..03e0146722e 100644
--- a/test/parallel/test-abortsignal-drop-settled-signals.mjs
+++ b/test/parallel/test-abortsignal-drop-settled-signals.mjs
@@ -246,6 +246,31 @@ it('drops settled dependent signals when signal is composite', (t, done) => {
   }));
 });

+it('drops an observed composite of a composite once its sources are collected', async (t) => {
+  let controllers = [new AbortController(), new AbortController()];
+
+  // Only the outer composite is observed, so the inner one is unreachable and
+  // is collected while the sources are still alive.
+  const outerRef = (() => {
+    const outer = AbortSignal.any([
+      controllers[0].signal,
+      AbortSignal.any([controllers[1].signal]),
+    ]);
+    outer.addEventListener('abort', () => {});
+    return new WeakRef(outer);
+  })();
+
+  const kDependantSignals = Object.getOwnPropertySymbols(controllers[1].signal).find(
+    (s) => s.toString() === 'Symbol(kDependantSignals)'
+  );
+  t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 2);
+
+  await gcUntil('inner composite is collected', () => controllers[1].signal[kDependantSignals].size === 1);
+
+  controllers = null;
+  await gcUntil('outer composite is collected', () => outerRef.deref() === undefined);
+});
+
 it('drops settled signals even when there are listeners', (t, done) => {
   runWithOrphanListeners(limit, async (signalRefs) => {
     await gcUntil('all signals are GCed', () => {