Skip to content

Commit 95b07ef

Browse files
authored
Merge pull request #867 from microsoft/tyriar/657
fix: close inherited file descriptors in child process on Linux
2 parents d70a325 + 1b98ac6 commit 95b07ef

File tree

2 files changed

+67
-0
lines changed

2 files changed

+67
-0
lines changed

src/unix/pty.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
3838
#if defined(__linux__)
3939
#include <pty.h>
40+
#include <dirent.h>
41+
#include <sys/syscall.h>
4042
#elif defined(__APPLE__)
4143
#include <util.h>
4244
#elif defined(__FreeBSD__)
@@ -110,6 +112,40 @@ struct ExitEvent {
110112
int exit_code = 0, signal_code = 0;
111113
};
112114

115+
#if defined(__linux__)
116+
117+
static int
118+
SetCloseOnExec(int fd) {
119+
int flags = fcntl(fd, F_GETFD, 0);
120+
if (flags == -1)
121+
return flags;
122+
if (flags & FD_CLOEXEC)
123+
return 0;
124+
return fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
125+
}
126+
127+
/**
128+
* Close all file descriptors >= 3 to prevent FD leakage to child processes.
129+
* Uses close_range() syscall on Linux 5.9+, falls back to /proc/self/fd iteration.
130+
*/
131+
static void
132+
pty_close_inherited_fds() {
133+
// Try close_range() first (Linux 5.9+, glibc 2.34+)
134+
#if defined(SYS_close_range)
135+
if (syscall(SYS_close_range, 3, ~0U, CLOSE_RANGE_CLOEXEC) == 0) {
136+
return;
137+
}
138+
#endif
139+
140+
int fd;
141+
// Set the CLOEXEC flag on all open descriptors. Unconditionally try the first
142+
// 16 file descriptors. After that, bail out after the first error.
143+
for (fd = 3; ; fd++)
144+
if (SetCloseOnExec(fd) && fd > 15)
145+
break;
146+
}
147+
#endif
148+
113149
void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
114150
std::thread *th = new std::thread;
115151
// Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE.
@@ -433,6 +469,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
433469
}
434470
}
435471

472+
// Close inherited FDs to prevent leaking pty master FDs to child
473+
pty_close_inherited_fds();
474+
436475
{
437476
char **old = environ;
438477
environ = env;

src/unixTerminal.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,34 @@ if (process.platform !== 'win32') {
256256
});
257257
});
258258
describe('spawn', () => {
259+
if (process.platform === 'linux') {
260+
it('should not leak pty file descriptors to child processes', (done) => {
261+
// Spawn 3 ptys - the 3rd should not see FDs from the first two
262+
const ptys: UnixTerminal[] = [];
263+
for (let i = 0; i < 3; i++) {
264+
ptys.push(new UnixTerminal('/bin/bash', [], {}));
265+
}
266+
267+
let output = '';
268+
ptys[2].onData((data) => {
269+
output += data;
270+
});
271+
272+
// Check for ptmx FDs in the 3rd terminal's shell
273+
ptys[2].write('echo "PTMX_COUNT:$(file /proc/$$/fd/* 2>/dev/null | grep -c ptmx)"\n');
274+
275+
setTimeout(() => {
276+
for (const pty of ptys) {
277+
pty.kill();
278+
}
279+
// Extract the count from output - should be 0
280+
const match = output.match(/PTMX_COUNT:(\d+)/);
281+
assert.ok(match, `Could not find PTMX_COUNT in output: ${output}`);
282+
assert.strictEqual(match![1], '0', `Expected 0 ptmx FDs but got ${match![1]}`);
283+
done();
284+
}, 1000);
285+
});
286+
}
259287
if (process.platform === 'darwin') {
260288
it('should return the name of the process', (done) => {
261289
const term = new UnixTerminal('/bin/echo');

0 commit comments

Comments
 (0)