⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@Tyriar
Copy link
Member

@Tyriar Tyriar commented Jan 11, 2026

Prevents leaking file descriptors (pty master FDs, sockets, etc.) to child processes spawned via forkpty(). Uses close_range() syscall on Linux 5.9+, falling back to /proc/self/fd iteration, then brute force.

macOS already handles this via POSIX_SPAWN_CLOEXEC_DEFAULT flag.

Fixes #657
Part of microsoft/vscode#58814

Prevents leaking file descriptors (pty master FDs, sockets, etc.) to
child processes spawned via forkpty(). Uses close_range() syscall on
Linux 5.9+, falling back to /proc/self/fd iteration, then brute force.

macOS already handles this via POSIX_SPAWN_CLOEXEC_DEFAULT flag.

Fixes #657
Part of microsoft/vscode#58814
@Tyriar Tyriar added this to the 1.2.0 milestone Jan 11, 2026
@Tyriar Tyriar requested a review from deepak1556 January 11, 2026 06:39
@Tyriar Tyriar self-assigned this Jan 11, 2026
pty_close_inherited_fds() {
// Try close_range() first (Linux 5.9+, glibc 2.34+)
#if defined(SYS_close_range)
if (syscall(SYS_close_range, 3, ~0U, 0) == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can let the exec call take care of closing

Suggested change
if (syscall(SYS_close_range, 3, ~0U, 0) == 0) {
if (syscall(SYS_close_range, 3, ~0U, CLOSE_RANGE_CLOEXEC) == 0) {

Comment on lines +129 to +153
// Fallback: iterate /proc/self/fd
int dirfd = open("/proc/self/fd", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dirfd >= 0) {
DIR *dir = fdopendir(dirfd);
if (dir) {
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
if (entry->d_name[0] == '.') continue;
int fd = atoi(entry->d_name);
if (fd >= 3 && fd != dirfd) {
close(fd);
}
}
closedir(dir); // Also closes dirfd
return;
}
close(dirfd);
}

// Ultimate fallback: brute force (slow with high ulimit)
long max_fd = sysconf(_SC_OPEN_MAX);
if (max_fd < 0) max_fd = 1024;
for (int fd = 3; fd < max_fd; fd++) {
close(fd);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can mimic libuv https://github.com/libuv/libuv/blob/f420c2bd57ba79d21e1797324ed3ca18e8d92f89/src/unix/core.c#L800-L809 set cloexec flags on the fds and bail on the first error. The last call here to go over _SC_OPEN_MAX is really horrible for spawn performance.

Suggested change
// Fallback: iterate /proc/self/fd
int dirfd = open("/proc/self/fd", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dirfd >= 0) {
DIR *dir = fdopendir(dirfd);
if (dir) {
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
if (entry->d_name[0] == '.') continue;
int fd = atoi(entry->d_name);
if (fd >= 3 && fd != dirfd) {
close(fd);
}
}
closedir(dir); // Also closes dirfd
return;
}
close(dirfd);
}
// Ultimate fallback: brute force (slow with high ulimit)
long max_fd = sysconf(_SC_OPEN_MAX);
if (max_fd < 0) max_fd = 1024;
for (int fd = 3; fd < max_fd; fd++) {
close(fd);
}
int fd;
/* Set the CLOEXEC flag on all open descriptors. Unconditionally try the
* first 16 file descriptors. After that, bail out after the first error.
*/
for (fd = 3; ; fd++)
if (SetCloseOnExec(fd) && fd > 15)
break;

SetCloseOnExec would be as follows,

int SetCloseOnExec(int fd) {
  int flags = fcntl(fd, F_GETFD, 0);
  if (-1 == flags)
    return flags;
  if (flags & FD_CLOEXEC)
    return 0;
  return fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leaking FDs: Close non-std FDs between pty_forkpty and pty_execvpe

3 participants