You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Background node process corrupts terminal state with tcsetattr() on exit #35536
This launches node -e 'setTimeout(() => {}, 2000)' in the background and sleep 1 in the foreground. After 1 second, control returns to Bash. After 2 seconds, Node exits and corrupts Bash’s terminal state:
the ↑, ↓, →, ← arrow keys start printing ^[[A, ^[[B, ^[[C, ^[[D instead of scrolling through the Bash history or moving the insert point,
the Tab key starts moving the cursor 8 spaces forward instead of Tab-completing the command,
Ctrl+A and Ctrl+E print ^A and ^E instead of jumping to the beginning and end of the command, etc.
This happens when the ResetStdio handler uses tcsetattr to “restore” the terminal to the state it was in when Node started (#24260). This interferes with Bash, which uses and expects a different terminal state than sleep 1.
(The bug is sometimes reproducible without the sleep 1 depending on whether Node records the initial terminal state before Bash updates it, but the sleep 1 makes it reliable.)
Normally, the terminal state would be protected from such undesired modifications by a background process: the kernel generates SIGTTOU to suspend the process until it’s brought to the foreground. But ResetStdio now deliberately overrides this protection by blocking SIGTTOU (#28535).
IMO, both #24260 and #28535 should be reverted. Node is a programming language; it should never make changes to the terminal state that programs did not request.
This also reproduces in Node 20.5.1 and 18.16.1 by:
Run any Node.js process in the foreground
Ctrl-Z it
Background it by entering bg
The process subsequently exits while backgrounded
I have a probably unusual use case where I hit this a lot and it's frustrating (I wrote a simple music player using Node that exits after playing the song). I concur that the behavior should be changed.
I don't think reverting #24260 or #28535 is realistically an option because that impacts a lot more users than this bug report. I personally don't care if node suspends on SIGTTOU or not, but enough people did to flag it as a bug.
My advice is to simply not background node but, if you absolutely have to, then as a workaround, reopen file descriptors 0-2 to /dev/null right before exiting. That will stop node from issuing the tcsetattr() calls.
Unless you or someone else has an actionable way forward, I'll go and close this issue in a few days.
Of course #28535 impacts many users given #24260. That’s why the proposal is to revert #24260and#28535. If Node does not make unrequested changes to the terminal state on exit, then there will be no SIGTTOU and no suspension.
This issue affects more people than those who realize it, because it is so unexpected. It’s not just about a Node program running in the background; that’s just the most reliable way to reproduce. It also makes for a race condition that nondeterministically breaks piping a Node program to a pager, because the Node program exits while the pager is still supposed to be in charge of the terminal mode. See for example
Node programs that intentionally modify the terminal state are rare. It is the responsibility of those programs to restore the terminal state, and it’s reasonable to expect users to avoid running those programs in the background or piping them to a pager. But it’s not reasonable to say that typical programs that just console.log() things can’t safely be backgrounded or piped like programs in any other language.
Reacted by Scott Feeney, Matt Kantor, chocolateboy and Jesper Ek
This issue affects more people than those who realize it, because it is so unexpected.
That seems true to me. As a case in point, I've been hitting this bug regularly for years, and it only just occurred to me to try to isolate the cause, figuring out it was a bug in Node and not my own program, and perform some lucky web searches that happened to lead me to this issue.
I skimmed #24260 and the issues it was trying to solve. I might be a little out of my depth here, but would it be reasonable to only make this tcsetattr() call on exit if stdin/stdout had previously been set to raw mode? Seems like it's trying to work around relatively specific cases where specific calls were made by the program.
That’s why the proposal is to revert #24260 and #28535.
Just to be clear: I'm saying reverting isn't an option. We're essentially at a local optimum where things work well enough for most users, and while I sympathize with your use case, anything that negatively impacts the silent majority is a no-go.
But it’s not reasonable to say that typical programs that just console.log() things can’t safely be backgrounded
With all due respect, you're betraying a lack of insight here in how node works under the hood. It goes to great lengths to provide an async runtime and that includes things like console.log.
would it be reasonable to only make this tcsetattr() call on exit if stdin/stdout had previously been set to raw mode?
I considered that when I wrote #24260 but it's unreliable with libraries like blessed (popular, ca. 10M downloads/month) because not all tty state changes are visible to node. The unconditional reset acts as a fail-safe that restores it to a sane state on program exit.
A command line flag to disable the current behavior might be an acceptable way forward (because that makes it opt-in) but to set expectations, I don't plan on working on or reviewing that myself, you'll have to find someone else.
This issue affects more people than those who realize it, because it is so unexpected.
Yes, I was bitten by this when piping the output from my node process (ran using --watch) and couldn't understand why terminal got messed up when the process restarted. Took me a long time to understand that node ResetStdio was the culprit.
I considered that when I wrote #24260 but it's unreliable with libraries like blessed (popular, ca. 10M downloads/month) because not all tty state changes are visible to node.
Funnily enough, the way I discovered this was when piping log output from my node applications, to a "log viewer" I had written using blessed (so the issue was just moved...)
My advice is to simply not background node but, if you absolutely have to, then as a workaround, reopen file descriptors 0-2 to /dev/null right before exiting. That will stop node from issuing the tcsetattr() calls.
This essentially breaks my application, I dont think it's reasonable to tell users to modify their when running in background.
I think the current behavior of relying on node to reset the state has probably put us in a position where we cant just revert the change.
Can I suggest that node only restores tty state when it's running in foreground?
I know this is very old now, but it's still a real and pressing issue. Here's a repro that's perhaps simpler:
``zsh
% unsetopt notify
% cat <(node -e 1) &
This will cause the terminal to be stuck in cooked mode (so pressing arrow keys shows ANSI sequences instead of their normal behavior) on the next prompt. The `unsetopt notify` is necessary because otherwise Zsh reports the completed job and gives you a new prompt, at which points it resets the terminal mode. Alternatively, you can disown the job:
```zsh
% cat <(node -e 1) &!
Note that this bug is also recently cropping up when piping Node's output to less. That's recently generating noise because less made a change that's now exposing Node's poor behavior in this regard. Even node -h | less demonstrates the problem, if you have less version 710.
Please fix this long-standing bug. Node should not be changing terminal state it didn't set.
Activity
run-dev: Wait for children to exit on Ctrl+C instead of killing them.
run-dev: Wait for children to exit on Ctrl+C instead of killing them.
run-dev: Wait for children to exit on Ctrl+C instead of killing them.
run-dev: Wait for children to exit on Ctrl+C instead of killing them.
run-dev: Wait for children to exit on Ctrl+C after killing them.
run-dev: Wait for children to exit on Ctrl+C after killing them.
run-dev: Wait for children to exit on Ctrl+C after killing them.
run-dev: Wait for children to exit on Ctrl+C after killing them.
run-dev: Wait for children to exit on Ctrl+C after killing them.
gireeshpunathil commentedon Jul 11, 2021
able to reproduce in linux, not sure what implications are for reverting the two commits in the linked PR. tag. Tagging as a bug.
graue commentedon Aug 19, 2023
This also reproduces in Node 20.5.1 and 18.16.1 by:
bgI have a probably unusual use case where I hit this a lot and it's frustrating (I wrote a simple music player using Node that exits after playing the song). I concur that the behavior should be changed.
bnoordhuis commentedon Aug 19, 2023
I don't think reverting #24260 or #28535 is realistically an option because that impacts a lot more users than this bug report. I personally don't care if node suspends on SIGTTOU or not, but enough people did to flag it as a bug.
My advice is to simply not background node but, if you absolutely have to, then as a workaround, reopen file descriptors 0-2 to /dev/null right before exiting. That will stop node from issuing the tcsetattr() calls.
Unless you or someone else has an actionable way forward, I'll go and close this issue in a few days.
andersk commentedon Aug 19, 2023
Of course #28535 impacts many users given #24260. That’s why the proposal is to revert #24260 and #28535. If Node does not make unrequested changes to the terminal state on exit, then there will be no
SIGTTOUand no suspension.This issue affects more people than those who realize it, because it is so unexpected. It’s not just about a Node program running in the background; that’s just the most reliable way to reproduce. It also makes for a race condition that nondeterministically breaks piping a Node program to a pager, because the Node program exits while the pager is still supposed to be in charge of the terminal mode. See for example
resetif used withfzfsharkdp/bat#1650Node programs that intentionally modify the terminal state are rare. It is the responsibility of those programs to restore the terminal state, and it’s reasonable to expect users to avoid running those programs in the background or piping them to a pager. But it’s not reasonable to say that typical programs that just
console.log()things can’t safely be backgrounded or piped like programs in any other language.graue commentedon Aug 19, 2023
That seems true to me. As a case in point, I've been hitting this bug regularly for years, and it only just occurred to me to try to isolate the cause, figuring out it was a bug in Node and not my own program, and perform some lucky web searches that happened to lead me to this issue.
I skimmed #24260 and the issues it was trying to solve. I might be a little out of my depth here, but would it be reasonable to only make this
tcsetattr()call on exit if stdin/stdout had previously been set to raw mode? Seems like it's trying to work around relatively specific cases where specific calls were made by the program.bnoordhuis commentedon Aug 19, 2023
Just to be clear: I'm saying reverting isn't an option. We're essentially at a local optimum where things work well enough for most users, and while I sympathize with your use case, anything that negatively impacts the silent majority is a no-go.
With all due respect, you're betraying a lack of insight here in how node works under the hood. It goes to great lengths to provide an async runtime and that includes things like console.log.
I considered that when I wrote #24260 but it's unreliable with libraries like blessed (popular, ca. 10M downloads/month) because not all tty state changes are visible to node. The unconditional reset acts as a fail-safe that restores it to a sane state on program exit.
A command line flag to disable the current behavior might be an acceptable way forward (because that makes it opt-in) but to set expectations, I don't plan on working on or reviewing that myself, you'll have to find someone else.
ritschwumm commentedon Feb 8, 2024
might the proposed reverts fix #42433 too? i currently have a very strange case where this occurs when node should just cleanly exit.
deadbeef84 commentedon Feb 17, 2025
Yes, I was bitten by this when piping the output from my node process (ran using --watch) and couldn't understand why terminal got messed up when the process restarted. Took me a long time to understand that node
ResetStdiowas the culprit.Funnily enough, the way I discovered this was when piping log output from my node applications, to a "log viewer" I had written using blessed (so the issue was just moved...)
This essentially breaks my application, I dont think it's reasonable to tell users to modify their when running in background.
I think the current behavior of relying on node to reset the state has probably put us in a position where we cant just revert the change.
Can I suggest that node only restores tty state when it's running in foreground?
remove workaround for controlling terminal behavior in repl_cmd (#58554)
remove workaround for controlling terminal behavior in repl_cmd (#58554)
remove workaround for controlling terminal behavior in repl_cmd (#58554)
remove workaround for controlling terminal behavior in repl_cmd (#58554)
SamB commentedon Mar 2, 2026
Yeah, simply running
node --v8-options | lessand not scrolling to the end is enough to trigger this problem.Or, for that matter,
node --v8-options |& less.This doesn't really work well at all.
run-dev: Wait for children to exit on Ctrl+C after killing them.
anderson-pete commentedon Oct 11, 2026
I know this is very old now, but it's still a real and pressing issue. Here's a repro that's perhaps simpler:
``zsh
% unsetopt notify
% cat <(node -e 1) &
Note that this bug is also recently cropping up when piping Node's output to
less. That's recently generating noise becauselessmade a change that's now exposing Node's poor behavior in this regard. Evennode -h | lessdemonstrates the problem, if you havelessversion 710.Please fix this long-standing bug. Node should not be changing terminal state it didn't set.
fatihguzeldev commentedon Oct 11, 2026
i have an open PR adding
--no-restore-terminal-state:#66540
it skips the startup terminal settings restore while keeping libuv's raw-mode cleanup and O_NONBLOCK restoration. the default stays unchanged.
also we're discussing changing the default, including the compatibility concerns, in #66440.