Repository: microsoft/node-pty

Issue #657: Leaking FDs: Close non-std FDs between pty_forkpty and pty_execvpe
Created: 2024-01-21T06:49:54Z
Environment details

OS: Linux
OS version: Linux 6.7
node-pty version: The one bundled in VSCode, and I believe it still exists on master

Issue description
Related to microsoft/vscode#202558 .
Currently node-pty doesn't close unwanted fds before spawning the process in pty_execvpe. This caused the above issue where Node's open fd(s) accidentally leaks into the spawned process. Considering that very few spawned shell process are aware to the non-std fds, I doubt if this would ever be the desired behavior.
To reduce the amount of surprise, I hope it could do something like https://github.com/facebook/folly/blob/main/folly/Subprocess.cpp#L559 to close all unwanted fds before spawning the shell.

EDIT from @Tyriar:
Lots more information in #710

Duplicate issue #710: Every integrated terminal opens one more fd pointing to /dev/ptmx or /dev/pts/ptmx
Created: 2024-09-01T07:25:09Z
Does this issue occur when all extensions are disabled?: Yes


Version: 1.92.2 (user setup)
Commit: fee1edb8d6d72a0ddff41e5f71a671c23ed924b9
Date: 2024-08-14T17:29:30.058Z
Electron: 30.1.2
ElectronBuildId: 9870757
Chromium: 124.0.6367.243
Node.js: 20.14.0
V8: 12.4.254.20-electron.0
OS: Windows_NT x64 10.0.22631
Every new integrated terminal opens /dev/ptmx instead of reusing the old one or opening the new one after closing the old one. This bug occurs both in my WSL and remote Ubuntu server.
Steps to Reproduce when VS Code is connected to WSL (Debian):

Compile the following C++ code (assume the executable is a.out). It's a test program trying to allocate as many fds as possible. It also reports suspicious occupied fds.

#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/resource.h>
#include <unistd.h>
#include <unordered_set>
#include <vector>

int main() {
  // check rlimit
  int ret;
  struct rlimit rlimit_res;
  if ((ret = getrlimit(RLIMIT_NOFILE, &rlimit_res)) == -1) {
    perror("getrlimit");
    exit(EXIT_FAILURE);
  }
  printf("soft limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_cur);
  printf("hard limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_max);

  // try to open as many files as we can
  std::vector<int> opened_files{0, 1, 2};
  while (true) {
    int fd = open("/dev/zero", O_RDONLY);
    if (fd == -1) {
      if (errno == EMFILE) {
        break;
      } else {
        perror("open");
        exit(EXIT_FAILURE);
      }
    }
    opened_files.push_back(fd);
  }
  // check if sorted
  for (size_t i = 1; i < opened_files.size(); ++i) {
    assert(opened_files[i - 1] < opened_files[i]);
  }
  // find missing fds
  std::vector<int> missing_fds;
  {
    int maxfd = opened_files.back();
    std::unordered_set<int> st(opened_files.begin(), opened_files.end());
    for (int i = 0; i < maxfd; ++i) {
      if (!st.count(i)) {
        printf("missing fd %d\n", i);
        missing_fds.push_back(i);
      }
    }
  }
  printf("fd count: %zd\n", opened_files.size());
  if (!missing_fds.empty()) {
    printf("You can run the following commands:\n");
    printf("======================\n");
    long pid = getpid();
    bool first = true;
    for (int fd : missing_fds) {
      if (first) {
        printf("file /proc/%ld/fd/%d", pid, fd);
        first = false;
      } else {
        printf(" \\\n     /proc/%ld/fd/%d", pid, fd);
      }
    }
    printf("\n");
    printf("======================\n");
    printf("<Press enter to exit>\n");
    getchar();
  }
}

Open 3 terminals.
Run a.out in the 1st terminal. The result may be:

soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
fd count: 1048572
You can run the following commands:
======================
file /proc/30552/fd/19 \
     /proc/30552/fd/20 \
     /proc/30552/fd/21 \
     /proc/30552/fd/22
======================
<Press enter to exit>

Keep the program running and copy-paste the command from output (the file command and its arguments) to the 2nd terminal. The results may be:

/proc/30552/fd/19: symbolic link to /dev/urandom
/proc/30552/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/30552/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/30552/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log

Now the process in the first terminal can be terminated, but the terminal should be kept open.
Try running the program in the second terminal and paste the output in another terminal, the results may be (there is one more fd pointing to /dev/ptmx):

/proc/30971/fd/19: symbolic link to /dev/urandom
/proc/30971/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/30971/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/30971/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
/proc/30971/fd/23: symbolic link to /dev/ptmx

Now run the program in the third terminal and paste the output in another terminal, the results may be (one more fd pointing to /dev/ptmx, again):

/proc/31198/fd/19: symbolic link to /dev/urandom
/proc/31198/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/31198/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/31198/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
/proc/31198/fd/23: symbolic link to /dev/ptmx
/proc/31198/fd/24: symbolic link to /dev/ptmx
Conclusion: The first terminal has fds pointing to /dev/urandom and some log files. Every new terminal beginning with the second one opens one more fd pointing to /dev/ptmx and the subprocesses inherit the fds from the terminal.
Other observations: The occupied fds are fixed once the terminal is open. After closing some terminals, the occupied fds in the previously opened terminals are not released, but the fds are released for new terminals.
The results are reproducible on a remote Ubuntu server, except that there is no fd pointing to /dev/urandom, and every new terminal opens /dev/pts/ptmx instead of /dev/ptmx.
I've seen a similar issue: microsoft/vscode#182212, but it doesn't mention WSL or remote servers.

Closing comment (2024-12-17T18:07:26Z): /duplicate #657