Repository: node
Issue: spawn cleanup for a process with a SIGINT handler can leave stdin in echo mode
Labels: child_process

### Version

v21.5.0

### Platform

Linux  hostname 6.5.0-14-generic #14-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 14 14:59:49 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

### Subsystem

child_process

### What steps will reproduce the bug?

1. Place the following in a file called `gnarly.js`
```javascript
#!/usr/bin/env node
const cp = require("child_process");
if (process.env.CHILD) {
  process.once("SIGINT", () => {});
  process.stdin.resume();
} else {
  cp.spawn("./gnarly.js", {
    stdio: [process.stdin, "pipe", process.stderr],
    env: { CHILD: 1, ...process.env },
  });
}
```
2. `chmod +x gnarly.js`
3. Run `./gnarly.js`
4. interrupt it with ctrl-c
5. Try to use the up or down arrow keys in your shell after a second or so, and realize your shell is stuck in echo mode

### How often does it reproduce? Is there a required condition?

consistently, every time.

### What is the expected behavior? Why is that the expected behavior?

_No response_

### What do you see instead?

My shells no longer seem to be seeing input, and instead standard input seems to be stuck in "echo mode". 

zsh recovers after I SIGINT a couple more times. nushell outright gets "stuck" and no new input is visible.

### Additional information

I have had trouble capturing information about this. Tools like `strace` and `gdb` seem to take stdin and do things, leading to the problem "fixing itself" when being observed directly. I also was having trouble getting `gdb` to follow spawned children to see what was going on there.

The main thing, though, is that I'm not exactly sure what system call or the like could be causing my problem. I think it _might_ be related to the following `ioctl` call that was setting `ICRNL` .... but I could be totally off base (snippet of those calls). I couldn't really reliably reproduce it directly.

```
565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|ICRNL|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|ICRNL|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|ICRNL|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0565465 ioctl(0, TCSETSW, {c_iflag=BRKINT|ICRNL|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
```

EDIT: actually I believe the following to be the specific cause, I was able to finally get `strace` to do its thing

The following happens during teardown (on the second SIGINT, since the first one gets absorbed by the `process.once` thing).

```
[pid 697631] ioctl(0, TCGETS,{c_iflag=BRKINT|INLCR|ICRNL|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
[pid 697631] ioctl(0, TCSETS, {c_iflag=BRKINT|ICRNL|IXON|IMAXBEL|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
```
```
FOR c_iflag
NO LONGER HERE:
{'INLCR'}
Added:
{'IXON'}
----------------------
FOR  c_oflag
NO LONGER HERE:
set()
Added:
set()
----------------------
FOR  c_cflag
NO LONGER HERE:
set()
Added:
set()
----------------------
FOR  c_lflag
NO LONGER HERE:
set()
Added:
{'ICANON', 'ECHO'}
----------------------
```