Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/unix/pty.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
#if defined(__linux__)
#include <pty.h>
#include <dirent.h>
#include <sys/syscall.h>
#elif defined(__APPLE__)
#include <util.h>
#elif defined(__FreeBSD__)
Expand Down Expand Up @@ -110,6 +112,40 @@ struct ExitEvent {
int exit_code = 0, signal_code = 0;
};

#if defined(__linux__)

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

/**
* Close all file descriptors >= 3 to prevent FD leakage to child processes.
* Uses close_range() syscall on Linux 5.9+, falls back to /proc/self/fd iteration.
*/
static void
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, CLOSE_RANGE_CLOEXEC) == 0) {
return;
}
#endif

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;
}
#endif

void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
std::thread *th = new std::thread;
// Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE.
Expand Down Expand Up @@ -433,6 +469,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
}
}

// Close inherited FDs to prevent leaking pty master FDs to child
pty_close_inherited_fds();

{
char **old = environ;
environ = env;
Expand Down
28 changes: 28 additions & 0 deletions src/unixTerminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,34 @@ if (process.platform !== 'win32') {
});
});
describe('spawn', () => {
if (process.platform === 'linux') {
it('should not leak pty file descriptors to child processes', (done) => {
// Spawn 3 ptys - the 3rd should not see FDs from the first two
const ptys: UnixTerminal[] = [];
for (let i = 0; i < 3; i++) {
ptys.push(new UnixTerminal('/bin/bash', [], {}));
}

let output = '';
ptys[2].onData((data) => {
output += data;
});

// Check for ptmx FDs in the 3rd terminal's shell
ptys[2].write('echo "PTMX_COUNT:$(file /proc/$$/fd/* 2>/dev/null | grep -c ptmx)"\n');

setTimeout(() => {
for (const pty of ptys) {
pty.kill();
}
// Extract the count from output - should be 0
const match = output.match(/PTMX_COUNT:(\d+)/);
assert.ok(match, `Could not find PTMX_COUNT in output: ${output}`);
assert.strictEqual(match![1], '0', `Expected 0 ptmx FDs but got ${match![1]}`);
done();
}, 1000);
});
}
if (process.platform === 'darwin') {
it('should return the name of the process', (done) => {
const term = new UnixTerminal('/bin/echo');
Expand Down