From c7d4ff3f72099b32a55ba452f0ba41292ca18db0 Mon Sep 17 00:00:00 2001 From: Theodor Steiner <40017636+Theo-Steiner@users.noreply.github.com> Date: Wed, 1 Mar 2023 23:18:05 +0900 Subject: [PATCH] repl: fix .load infinite loop caused by shared use of lineEnding RegExp Since the lineEnding Regular Expression is declared on the module scope, recursive invocations of its `[kTtyWrite]` method share one instance of this Regular Expression. Since the state of a RegExp is managed by instance, alternately calling RegExpPrototypeExec with the same RegExp on different strings can lead to the state changing unexpectedly. This is the root cause of this infinite loop bug when calling .load on javascript files of certain shapes. PR-URL: https://github.com/nodejs/node/pull/46742 Fixes: https://github.com/nodejs/node/issues/46731 Reviewed-By: Kohei Ueno Reviewed-By: Antoine du Hamel --- lib/internal/readline/interface.js | 22 ++++++++----- ...est-readline-interface-recursive-writes.js | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-readline-interface-recursive-writes.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 9b8ff459fa3573..09354cdc158420 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -1330,18 +1330,22 @@ class Interface extends InterfaceConstructor { // falls through default: if (typeof s === 'string' && s) { + // Erase state of previous searches. + lineEnding.lastIndex = 0; let nextMatch = RegExpPrototypeExec(lineEnding, s); - if (nextMatch !== null) { - this[kInsertString](StringPrototypeSlice(s, 0, nextMatch.index)); - let { lastIndex } = lineEnding; - while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) { - this[kLine](); + // If no line endings are found, just insert the string as is. + if (nextMatch === null) { + this[kInsertString](s); + } else { + // Keep track of the end of the last match. + let lastIndex = 0; + do { this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); ({ lastIndex } = lineEnding); - } - if (lastIndex === s.length) this[kLine](); - } else { - this[kInsertString](s); + this[kLine](); + // Restore lastIndex as the call to kLine could have mutated it. + lineEnding.lastIndex = lastIndex; + } while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null); } } } diff --git a/test/parallel/test-readline-interface-recursive-writes.js b/test/parallel/test-readline-interface-recursive-writes.js new file mode 100644 index 00000000000000..3a0aee5be9d619 --- /dev/null +++ b/test/parallel/test-readline-interface-recursive-writes.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); + +common.skipIfDumbTerminal(); + +const readline = require('readline'); +const rli = new readline.Interface({ + terminal: true, + input: new ArrayStream(), +}); + +let recursionDepth = 0; + +// Minimal reproduction for #46731 +const testInput = ' \n}\n'; +const numberOfExpectedLines = testInput.match(/\n/g).length; + +rli.on('line', () => { + // Abort in case of infinite loop + if (recursionDepth > numberOfExpectedLines) { + return; + } + recursionDepth++; + // Write something recursively to readline + rli.write('foo'); +}); + + +rli.write(testInput); + +assert.strictEqual(recursionDepth, numberOfExpectedLines);