Skip to content

node:util: create aborted()'s listener without Function.prototype.bind - #42283

Merged
dylan-conway merged 2 commits into
mainfrom
claude/util-aborted-listener
Sep 16, 2026
Merged

dylan-conway merged 2 commits into
mainfrom
claude/util-aborted-listener

Conversation

@dylan-conway

Copy link
Copy Markdown
Member

What does this PR do?

util.aborted() built its abort listener with onAbortedCallback.bind(undefined, promise). bind is looked up on Function.prototype, so if user code has replaced Function.prototype.bind, that replacement is called with the internal onAbortedCallback as its receiver. onAbortedCallback is a private helper that assumes its argument is the promise aborted() created, and it is not meant to be reachable from user code.

The listener is now created by a small createAbortedListener(promise) helper that returns a closure. It does no user-visible property lookups, and being its own function it still keeps resource out of the listener's scope, which is what the bind was there for (the listener must not keep resource alive).

Behavior of aborted() is otherwise unchanged.

How did you verify your code works?

Added a test to test/js/node/util/test-aborted.test.ts that replaces Function.prototype.bind around a call to aborted() and asserts it is never invoked.

  • USE_SYSTEM_BUN=1 bun test test/js/node/util/test-aborted.test.ts (Bun 1.4.2): the new test fails with [Function: onAbortedCallback] captured, the other 7 pass.
  • I have not run the file against a debug build of this branch; relying on CI for that.

…e.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.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 7889e32e-e454-4943-ac70-a78890110cd6

📥 Commits

Reviewing files that changed from the base of the PR and between 6b394bf and 278acb4.

📒 Files selected for processing (2)
  • src/js/node/util.ts
  • test/js/node/util/test-aborted.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

The aborted implementation now creates its event listener with a closure instead of Function.prototype.bind. A regression test verifies that replacing bind does not intercept listener setup.

Changes

Abort listener handling

Layer / File(s) Summary
Closure-based listener and regression coverage
src/js/node/util.ts, test/js/node/util/test-aborted.test.ts
createAbortedListener returns a closure over the promise. aborted registers this listener without bind. The test patches Function.prototype.bind and verifies that no receiver is captured.

Suggested reviewers: robobun

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 278ac

The abort-listener update has no identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: creating the listener without using Function.prototype.bind.
Description check ✅ Passed The description includes both required sections. It explains the change, the reason for the change, preserved behavior, the regression test, and the verification command. It also clearly notes that de…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, correct primordial-safety hardening.

What was reviewed:

  • Confirmed createAbortedListener being its own function keeps resource out of the closure's scope, so the FinalizationRegistry on resource still fires (an inline arrow inside aborted() would have retained it and regressed the leak fix).
  • The arrow closure discards the abort event argument just as the old bind(undefined, promise) path effectively did; onAbortedCallback only reads promise.
  • Test restores Function.prototype.bind in finally, asserts an exact empty receivers array, and lives in the existing test-aborted.test.ts.
Extended reasoning...

Overview

This PR touches two files: src/js/node/util.ts replaces the onAbortedCallback.bind(undefined, promise) call in util.aborted() with a new createAbortedListener(promise) helper that returns () => onAbortedCallback(promise), and test/js/node/util/test-aborted.test.ts gains one test that patches Function.prototype.bind and asserts aborted() never routes through it. No native code, no API surface change, no behavioral change for well-behaved callers.

Security risks

The change is itself a security/robustness hardening — it removes a user-observable Function.prototype.bind lookup from a builtin, which per REVIEW.md and src/js/CLAUDE.md is exactly the class of tamper-vector builtins must avoid. The main correctness risk to check was whether swapping .bind() for a closure could re-introduce the resource retention the original bind was there to avoid: it does not, because the closure is created in a separate top-level function whose only parameter is promise. The two-line comment states both non-obvious "why"s, satisfying the load-bearing-comment rule.

Level of scrutiny

Low-to-moderate. The diff is ~15 lines, mechanical, and follows the sanctioned pattern for primordial-safe builtins (there is no $bind intrinsic, so a closure is the right substitute). The listener's call signature change (arrow ignores the event arg vs. bind appending it) is immaterial since onAbortedCallback reads only its first parameter. The addEventListener options object and resistStopPropagation wrapper are unchanged.

Other factors

Test hygiene is good: added to the existing module test file, mutated global restored in finally, exact-value assertion that would fail on the pre-fix code (author confirmed USE_SYSTEM_BUN=1 captures onAbortedCallback). CODEOWNERS only covers *.d.ts, so no owner gate applies. No prior reviewer objections in the timeline, and the bug hunt exited on dry_streak with no findings.

@robobun

robobun commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator
Updated 9:53 AM PT - Sep 16th, 2026

@dylan-conway, your commit d2dd875 is building: #116611

@dylan-conway
dylan-conway merged commit bec98aa into main Sep 16, 2026
4 of 5 checks passed
@dylan-conway
dylan-conway deleted the claude/util-aborted-listener branch September 16, 2026 16:55
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.

2 participants