Skip to content

fs: fix FileHandle close listener/ref leak in streams #60316

fs: fix FileHandle close listener/ref leak in streams

fs: fix FileHandle close listener/ref leak in streams #60316

Triggered via pull request September 17, 2026 09:32
Status Failure
Total duration 1m 31s
Artifacts

commit-lint.yml

on: pull_request_target
lint-commit-message
1m 8s
lint-commit-message
Fit to window
Zoom out
Zoom in

Annotations

1 error
lint-commit-message
# 5397a5b5a3d93b9bde7c26eaf107226d2ff14cb9 ok 1 assisted-by-is-trailer: no Assisted-by metadata ok 2 co-authored-by-is-trailer: no Co-authored-by metadata ok 3 fixes-url: skipping fixes-url # SKIP not ok 4 line-after-title: blank line expected after title (s/node/pull/64229) --- { found: 's/node/pull/64229', compare: '===', wanted: '', at: { line: 1, column: 0, body: [ 's/node/pull/64229', 'Signed-off-by: Rasad Regmi <rasad@workalaya.com>', '', '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 (#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: https://github.com/nodejs/node/issues/64214', 'Refs: https://github.com/nodejs/node/pull/64227', 'Refs: https://github.com/nodejs/node/pull/65387', 'Refs: https://github.com/nodejs/node/pull/64229', 'Signed-off-by: Rasad Regmi <rasad@workalaya.com>', '', '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