From 2a54be19f62478ac48c0c6b28a02e591e3d4937e Mon Sep 17 00:00:00 2001 From: Jon Christiansen <467023+theJC@users.noreply.github.com> Date: Sun, 3 May 2026 18:23:43 -0500 Subject: [PATCH 1/2] fix(unix): validate spawn-helper exists before calling pty.fork() helperPath is computed once at module load time using __dirname. If the application binary moves after the process starts (e.g. a dev build whose repo directory is relocated while the app is running), helperPath becomes stale and posix_spawn returns ENOENT. Previously this propagated into the native layer as 'posix_spawnp failed.' with no path information, and in some cases left V8 heap objects in an inconsistent state that caused a fatal EXC_BREAKPOINT crash ~4 seconds later during a GC pass. This check throws a clean JS error before entering native code, keeps the process alive, and includes the resolved path to aid diagnosis. --- src/unixTerminal.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 2776d501e..61088fd3a 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts @@ -103,6 +103,9 @@ export class UnixTerminal extends Terminal { }; // fork + if (!fs.existsSync(helperPath)) { + throw new Error(`node-pty spawn-helper not found at '${helperPath}'. This can happen if the application binary has moved since the process started.`); + } const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit); this._socket = new tty.ReadStream(term.fd); From 066636ba543301dbb875a95c61d93b08e56a5372 Mon Sep 17 00:00:00 2001 From: Jon Christiansen <467023+theJC@users.noreply.github.com> Date: Sun, 3 May 2026 18:30:32 -0500 Subject: [PATCH 2/2] test(unix): add test for spawn-helper missing error on macOS --- src/unixTerminal.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/unixTerminal.test.ts b/src/unixTerminal.test.ts index f13e66d41..cfdf45a75 100644 --- a/src/unixTerminal.test.ts +++ b/src/unixTerminal.test.ts @@ -366,6 +366,25 @@ if (process.platform !== 'win32') { done(); }); }); + it('should throw a descriptive error when spawn-helper is missing', () => { + const { loadNativeModule } = require('./utils'); + const nativeModule = loadNativeModule('pty'); + const spawnHelperPath = path.resolve(__dirname, nativeModule.dir + '/spawn-helper'); + const backupPath = spawnHelperPath + '.bak'; + fs.renameSync(spawnHelperPath, backupPath); + try { + assert.throws( + () => new UnixTerminal('/bin/zsh', []), + (e: Error) => { + assert.ok(e.message.includes('spawn-helper not found'), `Unexpected message: ${e.message}`); + assert.ok(e.message.includes(spawnHelperPath), `Path missing from message: ${e.message}`); + return true; + } + ); + } finally { + fs.renameSync(backupPath, spawnHelperPath); + } + }); it('should not leak /dev/ptmx file descriptors after pty exit', async function(): Promise { this.timeout(30000);