Skip to content

helperPath replace produces 'app.asar.unpacked.unpacked/…' when caller is itself in app.asar.unpacked → posix_spawnp ENOENT #923

@arthur791004

Description

@arthur791004

Summary

src/unixTerminal.ts#L19-L20 resolves helperPath, then runs:

helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');

String.prototype.replace with a string argument replaces the first occurrence. When the resolved helperPath already contains app.asar.unpacked/… (because the caller of node-pty lives inside app.asar.unpacked too, not inside app.asar), the substring app.asar matches the prefix of app.asar.unpacked and the replace produces a path like …/app.asar.unpacked.unpacked/node_modules/node-pty/build/Release/spawn-helper, which doesn't exist on disk.

posix_spawn then fails with ENOENT and node-pty throws the generic posix_spawnp failed., which is widely misdiagnosed as a code-signing / hardened-runtime / sandbox problem.

Reproduction

// simulate what unixTerminal.js does when the caller is itself unpacked
let helperPath = '/path/to/MyApp.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/build/Release/spawn-helper';
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
console.log(helperPath);
// → /path/to/MyApp.app/Contents/Resources/app.asar.unpacked.unpacked/node_modules/node-pty/build/Release/spawn-helper
//                                                            ^^^^^^^^^^^^^^^^^^^ bogus

This affects packaged Electron apps that:

  1. Run node-pty from a separate child process spawned with ELECTRON_RUN_AS_NODE=1 (which strips Electron's asar shim and uses plain Node module resolution), AND
  2. Have that child binary itself living in app.asar.unpacked.

In that combination __dirname resolves to the real app.asar.unpacked/… filesystem path rather than the asar-shimmed app.asar/… path, so the replace runs against a string that already contains app.asar.unpacked. VS Code, Theia, etc. don't hit this because they call node-pty from the Electron main/renderer process where the asar shim is in play and __dirname reports app.asar/….

Suggested fix

Guard each replace so it doesn't fire when the unpacked path is already present:

if (helperPath.indexOf('app.asar.unpacked') === -1) {
  helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
}
if (helperPath.indexOf('node_modules.asar.unpacked') === -1) {
  helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
}

Or anchor the replace with a path-separator regex, e.g. .replace(/app\.asar([\\/])/, 'app.asar.unpacked$1').

Bonus: misleading error

src/unix/pty.cc#L373 reports posix_spawnp failed. without the underlying errno. Surfacing strerror(err) (this case would say No such file or directory) would have made this trivially diagnosable instead of sending people down the code-signing rabbit hole. Happy to send a separate PR for that.

Environment

  • node-pty 1.1.0
  • macOS arm64, Electron 32 (also reproduces with system Node ≥18 when require'ing node-pty from a path inside app.asar.unpacked)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions