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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions