Skip to content

fix(ext/web): prevent GC of AbortSignal.any()#32118

Merged
fraidev merged 2 commits into
denoland:mainfrom
fraidev:fix/dangling-promises-non-interactive
Feb 11, 2026
Merged

fix(ext/web): prevent GC of AbortSignal.any()#32118
fraidev merged 2 commits into
denoland:mainfrom
fraidev:fix/dangling-promises-non-interactive

Conversation

@fraidev

@fraidev fraidev commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Closes #32095

Introduced with V8 bump #31873

The new exposed AbortSignal.any(), stores dependent signals via WeakRef in the source signal's dependentSignals set. When the dependent signal is created as a temporary (e.g. AbortSignal.any([...]).addEventListener("abort", cb)), there are no strong references to it. V8's GC can collect it, destroying the abort listener and leaving promises unresolvable.

The fix stores a strong reference to dependent signals on the source signal (via an activeDependents set) whenever addEventListener refs a source timer. The strong reference is cleaned up in removeEventListener and runAbortSteps.

@fraidev
fraidev force-pushed the fix/dangling-promises-non-interactive branch from 31cb5d3 to 86636d4 Compare February 10, 2026 19:37
@fraidev
fraidev force-pushed the fix/dangling-promises-non-interactive branch from 86636d4 to a1800c1 Compare February 10, 2026 19:39
@fraidev
fraidev merged commit 652d4d2 into denoland:main Feb 11, 2026
23 checks passed
@iluha168

Copy link
Copy Markdown

❤️

littledivy added a commit that referenced this pull request May 29, 2026
… GC (#34516)

## Summary

Adds a regression test that mirrors the report in #29075: take
`AbortSignal.any()` of an `AbortController`-backed signal, register
an `"abort"` listener on the returned temporary, force a major GC,
then abort the controller and assert the listener fires.

Per the spec

([abort-signal-garbage-collection](https://dom.spec.whatwg.org/#abort-signal-garbage-collection)),
a non-aborted dependent `AbortSignal` must not be GC'd while its
source signals set is non-empty and it has registered listeners for
the `abort` event. The fix landed in #34061 — `addEventListener` now
pins the dependent into every source signal's `activeDependents`
set (not just timer sources). Without that pin, the temporary
returned from `AbortSignal.any([ac.signal])` is only referenced via
`WeakRef` from `ac.signal`'s `dependentSignals`, so V8 collects it
and the listener silently disappears.

The existing regression test
(`tests/specs/run/abort_signal_any_gc`) only exercises the
timer-source path: it includes `AbortSignal.timeout(500)` in the
source list, which the partial fix in #32118 already covered. This
new test exercises the controller-only path that the issue
reported.

Verified the test:
- prints `Aborted` and passes on builds containing #34061
  (locally-built `main` and canary `c39514d10a`+)
- produces no output and would fail the assertion on Deno 2.7.14 and
  earlier (only the timer-source pinning was in place)

## Test plan

- [ ] `cargo test --test specs -- abort_signal_any_controller_gc`
- [ ] Existing `cargo test --test specs -- abort_signal_any_gc` still
passes
- [ ] WPT `AbortSignal.any.html` continues to pass

Closes denoland/orchid#260

Co-authored-by: divybot <[email protected]>
Co-authored-by: Divy Srivastava <[email protected]>
littledivy added a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
… GC (denoland#34516)

## Summary

Adds a regression test that mirrors the report in denoland#29075: take
`AbortSignal.any()` of an `AbortController`-backed signal, register
an `"abort"` listener on the returned temporary, force a major GC,
then abort the controller and assert the listener fires.

Per the spec

([abort-signal-garbage-collection](https://dom.spec.whatwg.org/#abort-signal-garbage-collection)),
a non-aborted dependent `AbortSignal` must not be GC'd while its
source signals set is non-empty and it has registered listeners for
the `abort` event. The fix landed in denoland#34061 — `addEventListener` now
pins the dependent into every source signal's `activeDependents`
set (not just timer sources). Without that pin, the temporary
returned from `AbortSignal.any([ac.signal])` is only referenced via
`WeakRef` from `ac.signal`'s `dependentSignals`, so V8 collects it
and the listener silently disappears.

The existing regression test
(`tests/specs/run/abort_signal_any_gc`) only exercises the
timer-source path: it includes `AbortSignal.timeout(500)` in the
source list, which the partial fix in denoland#32118 already covered. This
new test exercises the controller-only path that the issue
reported.

Verified the test:
- prints `Aborted` and passes on builds containing denoland#34061
  (locally-built `main` and canary `c39514d10a`+)
- produces no output and would fail the assertion on Deno 2.7.14 and
  earlier (only the timer-source pinning was in place)

## Test plan

- [ ] `cargo test --test specs -- abort_signal_any_controller_gc`
- [ ] Existing `cargo test --test specs -- abort_signal_any_gc` still
passes
- [ ] WPT `AbortSignal.any.html` continues to pass

Closes denoland/orchid#260

Co-authored-by: divybot <[email protected]>
Co-authored-by: Divy Srivastava <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deno 2.6.7 breaks Deno.test's detection of dangling promises in non-interactive shells (and VSCode extension)

3 participants