Skip to content

fs: remove FileHandle stream close listener - #64229

Closed
ishaanlabs-gg wants to merge 3 commits into
nodejs:mainfrom
ishaanlabs-gg:fix-filehandle-readstream-listener-leak
Closed

ishaanlabs-gg wants to merge 3 commits into
nodejs:mainfrom
ishaanlabs-gg:fix-filehandle-readstream-listener-leak

Conversation

@ishaanlabs-gg

Copy link
Copy Markdown

Fixes: #64214

This removes the extra FileHandle close listener that can be left behind by
streams created with autoClose: false.

The listener still lets the stream close if the FileHandle is closed first,
but autoClose: false streams now remove that listener and release the extra
stream reference when the stream finishes or closes on its own.

The change also keeps the default autoClose behavior covered so read and
write streams still close their FileHandle as before.

Tests:

  • ./out/Release/node --check lib/internal/fs/streams.js
  • ./out/Release/node test/parallel/test-fs-promises-file-handle-stream.js
  • python3 tools/test.py -J --mode=release parallel/test-fs-promises-file-handle-stream

@nodejs-github-bot nodejs-github-bot added fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run. labels Jul 1, 2026

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

Comment thread lib/internal/fs/streams.js Outdated
options.fd.on('close', FunctionPrototypeBind(stream.close, stream));
stream[kHandleCloseListener] = FunctionPrototypeBind(stream.close, stream);
stream[kHandleCloseListenerCleanup] = FunctionPrototypeBind(
cleanupHandleCloseListener, stream);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not needed if not autoclosing. move it inside the if.

@ishaanlabs-gg
ishaanlabs-gg force-pushed the fix-filehandle-readstream-listener-leak branch 2 times, most recently from 98b30c4 to 59c7463 Compare July 1, 2026 14:11
@ishaanlabs-gg

Copy link
Copy Markdown
Author

Addressed the review feedback by only installing the FileHandle close listener for autoClose: false streams.\n\nLocal verification:\n\n\npython3 tools/test.py -J parallel/test-fs-promises-file-handle-stream\n

Comment thread lib/internal/fs/streams.js Outdated
Comment on lines +162 to +165
stream.once('close', stream[kHandleCloseListenerCleanup]);
stream.once('end', stream[kHandleCloseListenerCleanup]);
stream.once('finish', stream[kHandleCloseListenerCleanup]);
stream.once('error', stream[kHandleCloseListenerCleanup]);

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.

I'm pretty sure you don't need to listen for close here, since there will always be either an end (reader) or finish (writer), or an error. Listening for close just doubles-up the call (which is fine because the function protects against being called multiple times, but unnecessary)

@ishaanlabs-gg
ishaanlabs-gg force-pushed the fix-filehandle-readstream-listener-leak branch from 968e2ad to 610d021 Compare July 2, 2026 10:32
@ishaanlabs-gg

Copy link
Copy Markdown
Author

Updated this to address the follow-up review: the stream no longer registers a FileHandle close listener. It now only keeps a completion/error cleanup callback to release the FileHandle ref after end, finish, or error.

Validation:

python3 tools/test.py -J parallel/test-fs-promises-file-handle-stream

Result: focused test passed.

@MikeMcC399

Copy link
Copy Markdown
Contributor

Issue #64214 has been resolved

See #64227

Closing this PR now as it has been superseded.

Thanks for your contribution, even though it was not finally used!

@MikeMcC399 MikeMcC399 closed this Aug 17, 2026
rasadregmi added a commit to rasadregmi/node that referenced this pull request Sep 17, 2026
s/node/pull/64229
Signed-off-by: Rasad Regmi <[email protected]>

Title: fs: fix FileHandle close listener/ref leak in streams

Description:

`fileHandle.createReadStream()`/`createWriteStream()` with
`autoClose: false` leaks a `'close'` listener (and an internal
reference) on the `FileHandle` every time the stream finishes on its
own, because `autoClose: false` also disables `autoDestroy`, so the
stream never reaches `_destroy()` — the only place that previously
released them. Enough such streams on one long-lived handle trips
`MaxListenersExceededWarning`.

`importFd()` in `lib/internal/fs/streams.js` now builds a single
idempotent `releaseHandleRef` shared between `FileHandleOperations
.close()` (the normal destroy path, unchanged for the default
`autoClose: true` behavior) and a `finished()`-based safety net
registered after the stream's readable/writable state is
initialized, for streams that finish without ever calling
`destroy()`. `finished()` correctly resolves on `'end'`/`'finish'`
alone when the stream won't emit `'close'` (see `willEmitClose()` in
`internal/streams/utils.js`), and the shared idempotency guard means
it's safe even if a stream is also explicitly closed after finishing
on its own — the exact scenario that caused a prior fix (nodejs#64227) to
fs: fix FileHandle close listener/ref leak in streams

createReadStream()/createWriteStream({ autoClose: false }) created
from a FileHandle attached a 'close' listener and took a reference
on the handle in importFd(), but only released either when the
stream went through _destroy(). Since autoClose: false also
disables autoDestroy, a stream that finishes on its own never
reaches _destroy(), so the listener and reference leaked. Creating
enough such streams on a long-lived handle (e.g. repeated ranged
reads) eventually tripped MaxListenersExceededWarning.

A prior fix (fixed in 64227, reverted in 65387) released the
reference again on the stream's 'end'/'finish'/'error' independent
of the destroy path, which could unref the handle twice if the
stream was later also explicitly closed/destroyed - a normal thing
to do after a stream naturally ends.

This introduces a single idempotent release function shared by both
paths (the normal destroy path and a finished()-based safety net for
autoClose: false streams that never reach _destroy()), so the
reference and listener are released exactly once regardless of how
the stream ends.

Fixes: nodejs#64214
Refs: nodejs#64227
Refs: nodejs#65387
Refs: nodejs#64229
Signed-off-by: Rasad Regmi <[email protected]>

Manual Git Steps (for you to run)

cd "/home/rasadregmi/Desktop/Open Source Contributions Repos/node"
git checkout -b fix-filehandle-stream-close-listener-leak main
git add lib/internal/fs/streams.js test/parallel/test-fs-promises-file-hand
git commit -s
git push fork fix-filehandle-stream-close-listener-leak

Commit Message

fs: fix FileHandle close listener/ref leak in streams

createReadStream()/createWriteStream({ autoClose: false }) created
from a FileHandle attached a 'close' listener and took a reference
on the handle in importFd(), but only released either when the
stream went through _destroy(). Since autoClose: false also
disables autoDestroy, a stream that finishes on its own never
reaches _destroy(), so the listener and reference leaked. Creating
enough such streams on a long-lived handle (e.g. repeated ranged
reads) eventually tripped MaxListenersExceededWarning.

A prior fix (fixed in 64227, reverted in 65387) released the
reference again on the stream's 'end'/'finish'/'error' independent
of the destroy path, which could unref the handle twice if the
stream was later also explicitly closed/destroyed - a normal thing
to do after a stream naturally ends.

This introduces a single idempotent release function shared by both
paths (the normal destroy path and a finished()-based safety net for
autoClose: false streams that never reach _destroy()), so the
reference and listener are released exactly once regardless of how
the stream ends.

Fixes: nodejs#64214
Refs: nodejs#64227
Refs: nodejs#65387
Refs: nodejs#64229

Signed-off-by: Rasad Regmi <[email protected]>
rasadregmi added a commit to rasadregmi/node that referenced this pull request Sep 17, 2026
createReadStream()/createWriteStream({ autoClose: false }) created
from a FileHandle attached a 'close' listener and took a reference
on the handle in importFd(), but only released either when the
stream went through _destroy(). Since autoClose: false also
disables autoDestroy, a stream that finishes on its own never
reaches _destroy(), so the listener and reference leaked. Creating
enough such streams on a long-lived handle (e.g. repeated ranged
reads) eventually tripped MaxListenersExceededWarning.

A prior fix (fixed in 64227, reverted in 65387) released the
reference again on the stream's 'end'/'finish'/'error' independent
of the destroy path, which could unref the handle twice if the
stream was later also explicitly closed/destroyed - a normal thing
to do after a stream naturally ends.

This introduces a single idempotent release function shared by both
paths (the normal destroy path and a finished()-based safety net for
autoClose: false streams that never reach _destroy()), so the
reference and listener are released exactly once regardless of how
the stream ends.

Fixes: nodejs#64214
Refs: nodejs#64227
Refs: nodejs#65387
Refs: nodejs#64229
Signed-off-by: Rasad Regmi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Close listener leak in fs/promises createReadStream

5 participants