Skip to content

lib: fix AbortSignal.any() observed-composite leak#64481

Open
bitpshr wants to merge 1 commit into
nodejs:mainfrom
bitpshr:lib/abortsignal-any-observed-leak
Open

lib: fix AbortSignal.any() observed-composite leak#64481
bitpshr wants to merge 1 commit into
nodejs:mainfrom
bitpshr:lib/abortsignal-any-observed-leak

Conversation

@bitpshr

@bitpshr bitpshr commented Jul 13, 2026

Copy link
Copy Markdown

Fixes a memory leak in AbortSignal.any() where an observed composite that follows a long-lived source keeps accumulating in that source's kDependantSignals, even across a forced global.gc().

The retainer is gcPersistentSignals. When you add an abort listener, the observed composite is added there so it stays alive long enough to fire. When one of its sources aborts, abortSignal() marks the composite aborted and runs its abort steps, but it never removes the composite from gcPersistentSignals. That set is only pruned when the listener is explicitly removed or a source is GC'd, so with a long-lived source the aborted composite is retained forever, never collected, and its WeakRef is never pruned from the source's dependant set.

An aborted composite can never fire again, so this drops each transitively-aborted dependent from gcPersistentSignals in the abort path, mirroring the removal that already happens for the directly-aborted signal a few lines down. Once it's collectable, the existing FinalizationRegistry prunes its entry from every source it followed.

The added test observes a long-lived source across many aborted composites and asserts the dependant set drains to zero. It fails before this change (the set stays at the iteration count) and passes after.

Fixes: #64476

An observed composite signal (one with an `abort` listener) is added to
gcPersistentSignals so it stays alive long enough to fire. When one of
its sources aborted, the composite was marked aborted but never removed
from that set, so it was retained forever and its WeakRef was never
pruned from a long-lived source's kDependantSignals. That set therefore
grew without bound.

Drop each transitively-aborted dependent from gcPersistentSignals, the
same way the directly-aborted signal is already removed, so it can be
collected and its dependant entries pruned.

Fixes: nodejs#64476
Signed-off-by: Paul Bouchon <mail@bitpshr.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbortSignal.any(): observed composites still accumulate in a long-lived source's kDependantSignals after #62367 (not GC-pruned)

2 participants