Skip to content

fix(captcha): 墓碑到期后留惰性存根,不再硬删全局 - #44

Merged
TriDefender merged 1 commit into
TriDefender:masterfrom
LX2000WASD:fix/captcha-tombstone-stubs
Sep 6, 2026
Merged

TriDefender merged 1 commit into
TriDefender:masterfrom
LX2000WASD:fix/captcha-tombstone-stubs

Conversation

@LX2000WASD

Copy link
Copy Markdown
Contributor

背景

v4.5.5 TUI 模式下反复崩溃(实际使用中间隔 2m47s / 14m37s / 27m15s / 29m15s):

zcode-proxy: tui crashed: ReferenceError: moveBy is not defined
    at tE (https://g.alicdn.com/captcha-frontend/FeiLin/1.5.1/feilin008….js:1:169658)

崩溃链条:

  1. FeiLin 的 tE 是个 2 秒心跳,会自我重新武装(feilin008.js:170382 tV=setInterval(tE,2e3))
  2. 它 arm 的定时器错误地落在宿主车道,happyDOM.close() 收不走 → 不朽定时器
  3. 30 秒后墓碑 delete 掉 moveBy,心跳下次触发即 ReferenceError
  4. TUI 的 uncaughtException 处理器对一切 exit(1) → 代理进程死亡

本 PR 切断第 3 环,与其余三个 PR 互不依赖,可单独合入。


问题

5fcb778 给 window 类全局加了 30 秒宽限期,让滞留的 FeiLin 指纹回调能平稳落地(feilin005.js 的 Text is not defined)。方向是对的。

但宽限期结束时仍然是 delete,所以它推迟了崩溃而没有消除崩溃:活得比 30 秒更久的残留回调,照样把一次全局读取变成 uncaughtException。

FeiLin 的心跳恰好属于这一类 —— 它每 2 秒重新武装自己,函数体里读 moveBy:

// feilin008.js:169658,tE 函数体内
Math.pow(!navigator*!moveBy, 0) ? r -= 1 : r += -6

只要这个心跳还活着,宽限期一到就是必然崩溃,只是把时刻从「拆解瞬间」推到了「拆解后 30 秒」。这与现场观测到的崩溃间隔分布一致。


改动

不再 delete,而是留下惰性值:

  • EXTRA_WINDOW_PROPS 里的窗口方法(moveBy/scrollTo/…)留一个 no-op —— 真实浏览器里它们本来也几乎是 no-op
  • 其余名字留 undefined

引用得以解析,调用无害完成。存根不指向已关闭的窗口,因此不会把它钉在 globalThis 上。


验证

让一个不朽心跳持续读 moveBy 跨越宽限期:

宽限期内 宽限期后
改动前 正常 每次触发一条 uncaughtException
改动后 正常 0

最坏情况下的直接调用也已确认无害:

墓碑到期后 typeof moveBy = function
straggler 调用 moveBy()          → 无害完成
FeiLin tE 里的原始表达式         → 无害求值
存根是否指向已关闭窗口            → 否
  • bun x tsc --noEmit 0 错误
  • bun test 733 pass / 0 fail(上游基线 732,本 PR 新增 1 条回归测试)

5fcb778 给 window 类全局加了 30 秒宽限期,让滞留的 FeiLin 指纹回调能平稳落地
(feilin005.js 的 "Text is not defined")。但宽限期结束时仍然是 delete,所以
它推迟了崩溃而没有消除崩溃:活得比 30 秒更久的残留回调照样把一次全局读取变成
uncaughtException。

FeiLin 的心跳恰好属于这一类。tE 每 2 秒重新武装自己一次
(feilin008.js:170382 `tV=setInterval(tE,2e3)`),函数体里读 `moveBy`
(:169658 `Math.pow(!navigator*!moveBy,0)`)。宽限期过后这一读就是致命的:

    ReferenceError: moveBy is not defined
        at tE (https://g.alicdn.com/…/feilin008….js:1:169658)

改为不 delete,而是留下惰性值:EXTRA_WINDOW_PROPS 里的窗口方法
(moveBy/scrollTo/…)留一个 no-op —— 真实浏览器里它们本来也几乎是 no-op;
其余名字留 undefined。引用得以解析,调用无害完成,且存根不指向已关闭的窗口,
不会把它钉在 globalThis 上。

验证:让一个不朽心跳持续读 moveBy 跨越宽限期,改动前每次触发都产生一条
uncaughtException,改动后为 0。

@TriDefender TriDefender left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已验证并合入:CI 绿 + 本地 worktree 复跑 tsc 0 错 / 733 pass(与声明一致)。存根不指向已关闭窗口、不把 closed window 钉在 globalThis 上,实现干净。注意:#46 的分支已逐字节包含本改动(INERT_WINDOW_METHODS + removeGlobalWindowAlias 的 stub 逻辑完全一致),两者谁先合入都不会冲突。

非阻塞 follow-up:INERT_WINDOW_METHODS 的注释说自己是 EXTRA_WINDOW_PROPS 的 "Subset",实际是全集;另外 undefined 存根对构造器类名(如 straggler 调 new Text())仍会 TypeError——只是从 ReferenceError 换了形态,#43 的分类器会兜底,记录在案即可。

@TriDefender
TriDefender merged commit 6d0064f into TriDefender:master Sep 6, 2026
1 check passed
LX2000WASD added a commit to LX2000WASD/zcode-api that referenced this pull request Sep 6, 2026
审阅指出这段文档写的是一个被推翻的中间方案——"Wrapping in an arrow-bodied
IIFE that RETURNS the inner expression"——而实现返回的是裸
`with(scope){…}` 语句,两者矛盾。

真实机制:`with` 是语句而非表达式,`eval` 返回它的块完成值,而块以最后一个
表达式语句完成,因此编译器交来的 `(function anonymous($happy_dom){…})`
原样流出,无需任何 IIFE 包装。(声明不产生完成值,所以以声明结尾的脚本同样
安全,沿用前一个表达式的值。)

注释同时保留选择 `with` 而非函数包装的另一半理由:`with` 引入的是对象环境
而非变量环境,guest 的顶层 var/function 声明得以继续外泄到全局对象。

顺带删除一处 rebase 到已合并的 TriDefender#44 之上时产生的 INERT_WINDOW_METHODS 重复
声明(tsc 不报此错,但运行时解析失败:"has already been declared")。
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.

2 participants