Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Windows: ConoutConnection worker and conpty_console_list_agent inherit process.execArgv, so the host's --require preload runs inside them

未关闭 适合新手
#974 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
78/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
node.js
领域
cli

调研方向

阅读 lib/windowsConoutConnection.js 和 lib/windowsPtyAgent.js,重点关注 issue 中描述的 Worker 和 fork 调用。在 Windows 上运行 watcher.cjs/host.cjs 复现,然后验证两个 helper 都使用空的 execArgv,并且 host 在其中不运行 preload 的情况下仍能存活。

由索引模型根据 Issue 内容生成。

描述

Summary

node-pty starts two helpers of its own on Windows and passes no execArgv to either:

  • lib/windowsConoutConnection.js — new worker_threads_1.Worker(<…>/worker/conoutSocketWorker.js, { workerData: workerData })
  • lib/windowsPtyAgent.js — child_process_1.fork(<…>/conpty_console_list_agent, [_this._innerPid.toString()])

Node defaults execArgv for both to process.execArgv, so every --require / --import / --loader / --inspect flag the embedding application put on its own command line is replayed inside node-pty's helpers. A host preload therefore runs a second time in a completely different environment, where the host's globals do not exist.

For a preload that is sensitive to that difference this is fatal, and it happens on terminal creation: WindowsPtyAgent's constructor builds the ConoutConnection worker synchronously, so the second run of the preload is part of "open a terminal".

Impact

Reproducible crash in an Electron desktop app. The wrapper launches the app's Node child with --require parent-watch.cjs, which keeps the child alive only while its stdin is open:

if (process.stdin !== null) {
  process.stdin.resume();
  process.stdin.once('end', () => process.kill(process.pid, 'SIGTERM'));
}

Inside the ConPTY output worker process.stdin is not null, but its 'end' event fires immediately. The watcher concludes its parent is gone and SIGTERMs the process. A worker thread shares the process, so the whole application dies with exit code 1 — no stderr, no uncaught exception, no Node crash report, nothing in the Windows event log. Opening the terminal kills the app, every time.

The stack never sees a JavaScript error, so from the embedder's side this is completely silent and extremely hard to attribute.

Versions

Both shipped layouts are affected; the compiled bytes differ but the defect is identical.

  • node-pty 1.1.0 — new worker_threads_1.Worker(path_1.join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData: workerData })
  • node-pty 1.2.0-beta.15 — new worker_threads_1.Worker((0, path_1.join)(scriptPath, 'worker/conoutSocketWorker.js'), { workerData: workerData })

Reproduction

The mechanism needs neither Electron nor node-pty — only the same call shape. Verified on Node v22.13.0, Windows 11 (build 26200).

watcher.cjs — the embedder preload:

if (process.stdin !== null) {
  process.stdin.resume();
  process.stdin.once('end', () => process.kill(process.pid, 'SIGTERM'));
}

host.cjs — stands in for WindowsPtyAgent:

const { Worker } = require('node:worker_threads');
const mode = process.argv[2];
new Worker(
  "require('node:fs').appendFileSync(process.env.LOG, 'worker ran, execArgv=' + JSON.stringify(process.execArgv) + '\\n')",
  { eval: true, ...(mode === 'empty' ? { execArgv: [] } : {}) },   // node-pty currently behaves like 'inherit'
);
setTimeout(() => require('node:fs').writeFileSync(process.env.LOG + '.survived', 'survived\n'), 4000);

A driver spawns host.cjs with --require watcher.cjs and stdio: ['pipe', 'pipe', 'pipe'], keeping the host's own stdin open for the whole run, so the only stdin that can end is the worker's.

With inherit — what node-pty does today:

[+32ms] threadId=0 preload loaded (stdin=stream)
[+34ms] host started execArgv=["--require","...watcher.cjs"]
[+57ms] threadId=1 preload loaded (stdin=stream)      <-- the preload ran again inside the worker
[+57ms] threadId=1 preload armed on stdin
worker body ran, execArgv=["--require","...watcher.cjs"]
[+59ms] threadId=1 preload fired: stdin ended -> SIGTERM
-> host exit code=1, signal=null, empty stderr

With execArgv: [] — the proposed fix:

[+31ms] host started execArgv=["--require","...watcher.cjs"]
worker body ran, execArgv=[]
[+55ms] worker exited code=0
[+4046ms] host survived

The forked agent inherits it too — the same probe shows the forked child's process.execArgv is ["--require", ".../noop-preload.cjs"] and the preload executes in it:

preload ran in pid=23736 threadId=0 execArgv=["--require","...noop-preload.cjs"]   <-- host
parent: execArgv=["--require","...noop-preload.cjs"]
preload ran in pid=41868 threadId=0 execArgv=["--require","...noop-preload.cjs"]   <-- forked child
forked child: execArgv=["--require","...noop-preload.cjs"] stdin=stream

Proposed fix

Give both helpers an explicit empty execArgv:

--- lib/windowsConoutConnection.js
+++ lib/windowsConoutConnection.js
-        this._worker = new worker_threads_1.Worker((0, path_1.join)(scriptPath, 'worker/conoutSocketWorker.js'), { workerData: workerData });
+        this._worker = new worker_threads_1.Worker((0, path_1.join)(scriptPath, 'worker/conoutSocketWorker.js'), { workerData: workerData, execArgv: [] });
--- lib/windowsPtyAgent.js
+++ lib/windowsPtyAgent.js
-            var agent = (0, child_process_1.fork)(path.join(__dirname, 'conpty_console_list_agent'), [_this._innerPid.toString()]);
+            var agent = (0, child_process_1.fork)(path.join(__dirname, 'conpty_console_list_agent'), [_this._innerPid.toString()], { execArgv: [] });
Why execArgv: [] and not filtering --require
  • Both helpers are internal plumbing — one pumps a named pipe, the other reads a console process list. They need none of the host's flags: no V8 options, no loaders, no inspector, no experimental switches.
  • A deny-list would break again on the next flag Node adds, and --require has several spellings (-r, --import, --experimental-loader, --loader).
  • It is also a boundary: a host's preload/loader should not be able to reach into a library's private helper processes at all.

The same reasoning applies to every other place node-pty uses fork or Worker; the two above are the ones on the Windows terminal path.

Workaround

Embedders currently apply an exact-byte postinstall patch to those two lines, and a host that also gates its preload on its own entry file happens to avoid the crash. Neither is a substitute for the fix in-tree.

One caveat I could not settle

execArgv: [] closes the command-line path. If a host injects its preload through NODE_OPTIONS=--require … instead, the worker shares process.env and new Worker has no env option, so I have not been able to rule that path out — it may be worth its own check while you are in here.

主要语言
TypeScript
星标
2k
派生
343
平均合并
22 小时 24 分钟
30 天内合并 PR
6

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/node-pty 的其他 Issue

查看 microsoft/node-pty 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。