Repository navigation
.load infinite loop in REPL #46731
Description
Version
v19.6.0
Platform
Darwin 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:43 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T8112 arm64
Subsystem
repl
What steps will reproduce the bug?
$ cat foo.js
function a(b) {
return b }
a(1)
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> .load foo.js
function a(b) {
function a(b) {
function a(b) {
...
This prints function a(b) { forever until killing the process (it doesn't respond to Ctrl-c)
How often does it reproduce? Is there a required condition?
Every time, with the correct file.
What is the expected behavior?
$ cat foo.js
function a(b) {
return b }
a(1)
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> .load foo.js
function a(b) {
return b }
a(1)
1
>
What do you see instead?
function a(b) {
function a(b) {
function a(b) {
...
Additional information
I'm struggling to figure out what's happening here. If the file contents don't include a new line after the first {, i.e.
function a(b) { return b }
a(1)
everything works fine. This lead me to believe it was somehow a parsing issue, but there are a number of iterations of the file that either do or do not work that don't seem to line up. The following files do work (separated by lines only containing //):
// same as above
function a(b) { return b }
a(1)
// not calling a
function a(b) {
return b }
// using arrow function
a = b => { return b }
// random example that works fine
a = 1 + 1
console.log(a)
The following do not work:
// same as above
function a(b) {
return b }
a(1)
// using arrow function (not calling a)
a = b => {
return b }
// using arrow function (calling a)
a = b => {
return b }
a(1)
I believe there were a few other weird things that worked and didn't work.
The odd thing is that reverting 6874aa1, lines 1554-1559 (6874aa1#diff-392b628f0a5eb047b1117351e6eedcefb8ec70a48933479e0e9bdc711a04f047L1554-L1559), seems to fix this. I tried a few other things, such as loading the file into a buffer before writing it out, toString()ing it after Buffer.from()ing it), changing the encoding, and changing the line endings. None of these worked.
Also, the tests run fine, even when modifying them to include the text that breaks outside of the tests.
Activity
harrisi commentedon Feb 19, 2023
The behavior is the same with v18.14.1 and v18.13.0, but works fine with v18.12.1. The only relevant change I can see between the two is from #45539, but I'm not sure why that would be causing this issue. It's also a little too in the weeds for me to spend much more time on it right now, unfortunately. Hopefully this information helps at least.
Theo-Steiner commentedon Feb 20, 2023
I did some testing and this seems to also relate to newline characters:
The infinite loop does not occur if the loaded file does not end with a newline character.
Note that even without the ending newline character, the output between v20.0.0-pre and 18.12.0 is different.
targos commentedon Feb 20, 2023
Bisected to #45614
/cc @aduh95
harrisi commentedon Feb 20, 2023
I'm confused by the new line situation. For one, you can see that without the newline it's not even reading the last line, apparently. Actually, this is interesting:
i.e., the repl is left in editor mode. That seems like a separate issue, however, so I'd be happy to open another issue if so.
cola119 commentedon Feb 20, 2023
Minimum reproduce code:
\n}\nTheo-Steiner commentedon Feb 21, 2023
I found the Problem!
Problem
The
lineEndingRegEx is declared on the module scope oflib/internal/readline/interface.js.This means that its state for calling
RegExpPrototypeExeccan be overwritten if called alternately on different strings.This is exactly what happens here for certain inputs, as the indentation preservation of the editor mode calls the same
writemethod inlib/internal/readline/interface.jsas theloadmethod from within a while loop insideload. Thus RegEx state is overwritten in a very unfortunate way, and we end up with an infinite loop.Detailed Description
loadcommand's action method is called with a file path ('foo.js').REPLServeritself, so we go up the prototype chain to the Interface.d = ' \n}\n', key = undefined.kTtyWrite, passing alongs = d, key = key.keyisundefinedwe fall right through to the default cases = ' \n}\n'we enter the top if clause and call exec on thelineEndingRegEx on our string s.lineEnding(/\r?\n|\r(?!\n)/g) matches \r\n, \n, or \r followed by something other than \n.nextMatch. With the minimal reproduction string of ' \n}\n'nextMatchis assigned['\n', index: 1, input: ' \n{\n', groups: undefined]nextMatchis not null, we enter the next if clause.kInsertStringwith a slice of the input string. We slice from 0 to the index ofnextMatch, which is 1 in our case, hence' 'is inserted.lineEndingRegEx that was set during our last invocation ofexec.execis called with s again, to get the nextMatch. The variable assignment in our case is:s = ' \n}\n',lastIndex = 2,nextMatch = ['\n', index: 3, input: ' \n{\n', groups: undefined].this[kLine]is called.[kLine]callsthis[kOnLine], which triggers a callback inlib/repl.jswith argumentcmd = line. In our case we inserted' 'into the line, socmd = ' '.' '.kTtyWriteagain from here with argumentsc = ' ', andkey = undefined.key=undefinedwe end up in the default case, but this time thelineEndingRegEx finds no match, so we jump [into the else clause.](
node/lib/internal/readline/interface.js
Line 1336 in b85b5ba
** However, the problem is that by using the same
lineEndingRegEx, it loses its state from the top while loop, where we already calledlineEnding.exec(s)and the next time we would call it, it would return null, since we iterated through all matches **[kInsertString]and adding' 'to the current line we return to the next line of theon line callbackininternal/repl.js.kLinein the first place.lastIndex = 2tonextMatch.index = 3, which means we add}to the current line.0as no match was found.lineEndingRegEx was reused in [No. 19] we getnextMatch = ['\n', index: 1, input: ' \n{\n', groups: undefined]again.lastIndex: 0andnextMatch: 1-> infinite looprepl: fix .load infinite loop caused by shared use of lineEnding RegExp
repl: fix .load infinite loop caused by shared use of lineEnding RegExp
repl: fix .load infinite loop caused by shared use of lineEnding RegExp
repl: fix .load infinite loop caused by shared use of lineEnding RegExp