Skip to content

Commit af053f2

Browse files
authored
fix: /dev/ptmx leak on macOS (#882)
* fix: /dev/ptmx leak on macOS * fix: flaky test * fix: ignore spawn_helper in process title getter
1 parent 598652c commit af053f2

File tree

3 files changed

+97
-31
lines changed

3 files changed

+97
-31
lines changed

src/unix/pty.cc

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ pty_posix_spawn(char** argv, char** env,
279279
const struct winsize *winp,
280280
int* master,
281281
pid_t* pid,
282-
int* err);
282+
std::string* err);
283283
#endif
284284

285285
struct DelBuf {
@@ -388,7 +388,7 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
388388
std::string helper_path = info[9].As<Napi::String>();
389389

390390
pid_t pid;
391-
int master;
391+
int master = -1;
392392
#if defined(__APPLE__)
393393
int argc = argv_.Length();
394394
int argl = argc + 4;
@@ -403,10 +403,13 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
403403
argv[i + 3] = strdup(arg.c_str());
404404
}
405405

406-
int err = -1;
406+
std::string err;
407407
pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
408-
if (err != 0) {
409-
throw Napi::Error::New(napiEnv, std::string("pty_posix_spawn failed with error: ") + std::to_string(err) + " (" + strerror(err) + ")");
408+
if (!err.empty()) {
409+
if (master != -1) {
410+
close(master);
411+
}
412+
throw Napi::Error::New(napiEnv, err);
410413
}
411414
if (pty_nonblock(master) == -1) {
412415
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
@@ -725,15 +728,26 @@ pty_getproc(int fd, char *tty) {
725728
#endif
726729

727730
#if defined(__APPLE__)
731+
static std::string format_error(const char* func, int err_code) {
732+
char buf[256];
733+
snprintf(buf, sizeof(buf), "%s: %s", func, strerror(err_code));
734+
return buf;
735+
}
736+
728737
static void
729738
pty_posix_spawn(char** argv, char** env,
730739
const struct termios *termp,
731740
const struct winsize *winp,
732741
int* master,
733742
pid_t* pid,
734-
int* err) {
743+
std::string* err) {
735744
int low_fds[3];
736745
size_t count = 0;
746+
int res = 0;
747+
int slave = -1;
748+
char slave_pty_name[128];
749+
int spawn_err;
750+
sigset_t signal_set;
737751

738752
for (; count < 3; count++) {
739753
low_fds[count] = posix_openpt(O_RDWR);
@@ -745,82 +759,103 @@ pty_posix_spawn(char** argv, char** env,
745759
POSIX_SPAWN_SETSIGDEF |
746760
POSIX_SPAWN_SETSIGMASK |
747761
POSIX_SPAWN_SETSID;
762+
763+
posix_spawn_file_actions_t acts;
764+
posix_spawn_file_actions_init(&acts);
765+
766+
posix_spawnattr_t attrs;
767+
posix_spawnattr_init(&attrs);
768+
748769
*master = posix_openpt(O_RDWR);
749770
if (*master == -1) {
750-
return;
771+
*err = format_error("posix_openpt failed", errno);
772+
goto done;
751773
}
752774

753-
int res = grantpt(*master) || unlockpt(*master);
775+
res = grantpt(*master);
754776
if (res == -1) {
755-
return;
777+
*err = format_error("grantpt failed", errno);
778+
goto done;
779+
}
780+
781+
res = unlockpt(*master);
782+
if (res == -1) {
783+
*err = format_error("unlockpt failed", errno);
784+
goto done;
756785
}
757786

758787
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
759-
int slave;
760-
char slave_pty_name[128];
761788
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
762789
if (res == -1) {
763-
return;
790+
*err = format_error("ioctl(TIOCPTYGNAME) failed", errno);
791+
goto done;
764792
}
765793

766794
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
767795
if (slave == -1) {
768-
return;
796+
*err = format_error("open slave pty failed", errno);
797+
goto done;
769798
}
770799

771800
if (termp) {
772801
res = tcsetattr(slave, TCSANOW, termp);
773802
if (res == -1) {
774-
return;
803+
*err = format_error("tcsetattr failed", errno);
804+
goto done;
775805
};
776806
}
777807

778808
if (winp) {
779809
res = ioctl(slave, TIOCSWINSZ, winp);
780810
if (res == -1) {
781-
return;
811+
*err = format_error("ioctl(TIOCSWINSZ) failed", errno);
812+
goto done;
782813
}
783814
}
784815

785-
posix_spawn_file_actions_t acts;
786-
posix_spawn_file_actions_init(&acts);
787816
posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO);
788817
posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO);
789818
posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO);
790819
posix_spawn_file_actions_addclose(&acts, slave);
791820
posix_spawn_file_actions_addclose(&acts, *master);
792821

793-
posix_spawnattr_t attrs;
794-
posix_spawnattr_init(&attrs);
795-
*err = posix_spawnattr_setflags(&attrs, flags);
796-
if (*err != 0) {
822+
spawn_err = posix_spawnattr_setflags(&attrs, flags);
823+
if (spawn_err != 0) {
824+
*err = format_error("posix_spawnattr_setflags failed", spawn_err);
797825
goto done;
798826
}
799827

800-
sigset_t signal_set;
801828
/* Reset all signal the child to their default behavior */
802829
sigfillset(&signal_set);
803-
*err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
804-
if (*err != 0) {
830+
spawn_err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
831+
if (spawn_err != 0) {
832+
*err = format_error("posix_spawnattr_setsigdefault failed", spawn_err);
805833
goto done;
806834
}
807835

808836
/* Reset the signal mask for all signals */
809837
sigemptyset(&signal_set);
810-
*err = posix_spawnattr_setsigmask(&attrs, &signal_set);
811-
if (*err != 0) {
838+
spawn_err = posix_spawnattr_setsigmask(&attrs, &signal_set);
839+
if (spawn_err != 0) {
840+
*err = format_error("posix_spawnattr_setsigmask failed", spawn_err);
812841
goto done;
813842
}
814843

815844
do
816-
*err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
817-
while (*err == EINTR);
845+
spawn_err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
846+
while (spawn_err == EINTR);
847+
if (spawn_err != 0) {
848+
*err = format_error("posix_spawn failed", spawn_err);
849+
}
818850
done:
819851
posix_spawn_file_actions_destroy(&acts);
820852
posix_spawnattr_destroy(&attrs);
853+
if (slave != -1) {
854+
close(slave);
855+
}
821856

822-
for (; count > 0; count--) {
823-
close(low_fds[count]);
857+
for (size_t i = 0; i <= count; i++) {
858+
close(low_fds[i]);
824859
}
825860
}
826861
#endif

src/unixTerminal.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,37 @@ if (process.platform !== 'win32') {
366366
done();
367367
});
368368
});
369+
it('should not leak /dev/ptmx file descriptors after pty exit', async function(): Promise<void> {
370+
this.timeout(30000);
371+
372+
const getPtmxFDCount = (): number => {
373+
try {
374+
const output = cp.execSync(`lsof -p ${process.pid} 2>/dev/null`, { encoding: 'utf8' });
375+
return output.split('\n').filter(line => line.includes('ptmx')).length;
376+
} catch {
377+
return 0;
378+
}
379+
};
380+
381+
const initialCount = getPtmxFDCount();
382+
for (let i = 0; i < 20; i++) {
383+
const term = new UnixTerminal('/bin/bash', ['-c', 'echo hello']);
384+
await new Promise<void>(resolve => {
385+
term.onExit(() => {
386+
term.destroy();
387+
resolve();
388+
});
389+
});
390+
}
391+
392+
await new Promise(r => setTimeout(r, 500));
393+
394+
const finalCount = getPtmxFDCount();
395+
assert.ok(
396+
finalCount <= initialCount,
397+
`Leaked ${finalCount - initialCount} /dev/ptmx FDs after spawning 20 PTYs (initial: ${initialCount}, final: ${finalCount})`
398+
);
399+
});
369400
}
370401
it('should handle exec() errors', (done) => {
371402
const term = new UnixTerminal('/bin/bogus.exe', []);

src/unixTerminal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export class UnixTerminal extends Terminal {
258258
public get process(): string {
259259
if (process.platform === 'darwin') {
260260
const title = pty.process(this._fd);
261-
return (title !== 'kernel_task') ? title : this._file;
261+
return (title !== 'kernel_task' && title !== 'spawn_helper') ? title : this._file;
262262
}
263263

264264
return pty.process(this._fd, this._pty) || this._file;

0 commit comments

Comments
 (0)