Skip to content

fix(git): harden runGit's output caps — latch, killTree, stderr budget - #999

Open
lhh010 wants to merge 1 commit into
omdsh-dev:devfrom
lhh010:fix/git-output-cap
Open

lhh010 wants to merge 1 commit into
omdsh-dev:devfrom
lhh010:fix/git-output-cap

Conversation

@lhh010

@lhh010 lhh010 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

PR:对 dev 已有的 runGit 输出上限做三点加固(闩锁 / killTree / stderr 预算)。

背景:2026-10-13 两次宿主崩溃(一手证据)

一个含未跟踪 pnpm junction 森林的工作区让 git status --porcelain -z --untracked-files=all 产出吉字节级输出,runGit 的 stdout 累积器冲破 V8 字符串上限,整个宿主以 RangeError: Invalid string length 崩溃(栈指向累积行,两次复现)。

dev 已有 GIT_OUTPUT_MAX_BYTES 字节上限与 NUL 字段计数封顶,但对照崩溃链仍有三个缺口:

# 缺口 后果
1 字节上限拒绝后无闩锁 拒绝后到达的 chunk 继续累加到已保留字符串上。Windows 上 spawn('git') 解析到 cmd shim,其孙进程在 child.kill() 后存活并继续灌数据(实测孤儿比 kill 多活 20+ 秒)——fieldCapped 只护行数封顶,字节封顶没有对应闩锁
2 kill 只杀包装器 孤儿孙进程需要管道被销毁(EPIPE)才真正停
3 stderr 无上限 诊断流冲破字符串上限的能力与答案流相同

修复

  • byteCapped 闩锁:字节封顶拒绝后,stdout/stderr 的后续 chunk 一律丢弃(fieldCapped || byteCapped 双闸);
  • killTree():销毁两条 stdio 流(EPIPE 终止孤儿)+ SIGKILL,超时/字节封顶/字段封顶三条 kill 路径统一使用;
  • stderr 纳入同一 maxBytes 预算。

病态工作区的最坏结果从「宿主死亡」降为「一次 SCM 刷新失败」。

验证

  • typecheck / build 通过;
  • 定向测试(archive / file-stream-route / git-remote-actions)43/44,唯一失败为已知机器特定的 symlink 用例(本机干净 dev 基线同样失败,与本变更无关);
  • 修复的局部热补丁版本已在真实宿主上运行 5 天(含一次对 junction 森林目录的直接触发验证:上限拒绝代替了崩溃)。

dev already caps stdout bytes (GIT_OUTPUT_MAX_BYTES) and NUL fields, but
three gaps remain, all proven by the 2026-10-13 host crashes (twice:
RangeError: Invalid string length at the stdout accumulator, a junction-
forest workspace driving 'git status -uall' to gigabytes):

1. no latch after a byte-cap reject — later chunks kept ACCUMULATING on
   the retained strings. On Windows spawn('git') resolves to the cmd
   shim, whose grandchild survives child.kill() and keeps feeding the
   dead accumulator (measured: the orphan outlived kill() by 20+s). The
   fieldCapped guard covered only the row cap; the byte cap had none.
2. kill() reaches only the wrapper — the orphaned grandchild needs its
   pipes destroyed (EPIPE) to actually stop. killTree() destroys both
   streams and is now used by every kill path (timeout, byte cap, field
   cap).
3. stderr was uncapped — a command whose DIAGNOSTICS stream past the
   budget is as able to kill the host via string length as its answer;
   stderr now shares the same maxBytes budget.

Worst case in a pathological workspace is now one failed SCM refresh,
never a dead host.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant