From 52e5e2d0da35a21235ae2b516f605508370e8865 Mon Sep 17 00:00:00 2001 From: sangwook Date: Fri, 6 Feb 2026 21:57:03 +0900 Subject: [PATCH 1/2] repl: fix FileHandle leak in history initialization Ensure that the history file handle is closed if initialization fails or flushing throws an error. This prevents ERR_INVALID_STATE errors where a FileHandle object is closed during garbage collection. --- lib/internal/repl/history.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index ed63ba01cb1be1..e95056ed5c466b 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -327,6 +327,7 @@ class ReplHistory { await this[kFlushHistory](); } catch (err) { + await this[kCloseHandle](); return this[kHandleHistoryInitError](err, onReadyCallback); } } From e87b6f401794aaacc13d981c3886296c42039698 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 7 Feb 2026 06:47:58 +0900 Subject: [PATCH 2/2] test: add regression test for repl history leak This adds a test case to verify that the file handle is properly closed when REPL history initialization fails, preventing resource leaks. Refs: 52e5e2d0da35a21235ae2b516f605508370e8865 --- .../test-repl-history-init-fail-leak.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/parallel/test-repl-history-init-fail-leak.js diff --git a/test/parallel/test-repl-history-init-fail-leak.js b/test/parallel/test-repl-history-init-fail-leak.js new file mode 100644 index 00000000000000..701a611ed2f385 --- /dev/null +++ b/test/parallel/test-repl-history-init-fail-leak.js @@ -0,0 +1,56 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const { ReplHistory } = require('internal/repl/history'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const historyPath = path.join(tmpdir.path, '.node_repl_history'); + +fs.writeFileSync(historyPath, 'dummy\n'); + +const originalOpen = fs.promises.open; +let closeCalled = false; + +fs.promises.open = async (filepath, flags, mode) => { + const handle = await originalOpen(filepath, flags, mode); + + if (flags === 'r+' && filepath === historyPath) { + handle.truncate = async (len) => { + throw new Error('Mock truncate error'); + }; + + const originalClose = handle.close; + handle.close = async () => { + closeCalled = true; + return originalClose.call(handle); + }; + } + + return handle; +}; + +const context = { + historySize: 30, + on: () => {}, + once: () => {}, + emit: () => {}, + pause: () => {}, + resume: () => {}, + off: () => {}, + line: '', + _historyPrev: () => {}, + _writeToOutput: () => {} +}; + +const history = new ReplHistory(context, { filePath: historyPath }); + +history.initialize(common.mustCall((err) => { + assert.strictEqual(err, null); + assert.strictEqual(closeCalled, true); +}));