From 2bb2e3c2170d5c6212ca79e95cf0e54fce8e7a57 Mon Sep 17 00:00:00 2001 From: WASD1999xl <2960525002@qq.com> Date: Sun, 6 Sep 2026 15:46:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=E5=A2=93=E7=A2=91=E5=88=B0?= =?UTF-8?q?=E6=9C=9F=E5=90=8E=E7=95=99=E6=83=B0=E6=80=A7=E5=AD=98=E6=A0=B9?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E7=A1=AC=E5=88=A0=E5=85=A8=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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。 --- src/proxy/captcha-happy.test.ts | 19 +++++++++++++++++++ src/proxy/captcha-happy.ts | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/proxy/captcha-happy.test.ts b/src/proxy/captcha-happy.test.ts index be615d9..7ecb411 100644 --- a/src/proxy/captcha-happy.test.ts +++ b/src/proxy/captcha-happy.test.ts @@ -225,6 +225,25 @@ describe("alias lifecycle (install/remove descriptor contract)", () => { expect(g.navigator).toBe(hostNavigator); }); + test("an expired tombstone leaves callable stubs, never a ReferenceError", async () => { + // The grace period ends, but guest stragglers can still be running: the + // FeiLin heartbeat `tE` re-arms itself every 2s and dereferences `moveBy` + // (feilin008.js:169658). A hard `delete` turned that read into an + // uncaught ReferenceError — fatal in the host realm, and the field crash + // this guards. The name must stay resolvable and callable. + const { g, w } = makeSandbox(); + (w as Record).moveBy = () => {}; + installGlobalWindowAlias(g, w, 15); + removeGlobalWindowAlias(g, w); + // Real elapsed time is the mechanism under test: the tombstone is a + // wall-clock grace period armed on the pristine host setTimeout. + await Bun.sleep(70); + expect(typeof g.moveBy).toBe("function"); + expect(() => (g.moveBy as () => void)()).not.toThrow(); + // The stub must not keep the closed window reachable from globalThis. + expect(g.moveBy).not.toBe((w as Record).moveBy); + }); + test("a new install cancels the pending tombstone", async () => { const { g, w } = makeSandbox(); const w2: Record = { diff --git a/src/proxy/captcha-happy.ts b/src/proxy/captcha-happy.ts index c82f300..79e8a18 100644 --- a/src/proxy/captcha-happy.ts +++ b/src/proxy/captcha-happy.ts @@ -1807,6 +1807,12 @@ const EXTRA_WINDOW_PROPS = [ "prompt", "getSelection", "find", ]; +// Subset of the above that a real browser implements as no-op-ish window +// methods. When the tombstone expires these become harmless stubs instead of +// being deleted, so a straggling guest callback that still calls `moveBy()` +// completes silently rather than raising a fatal ReferenceError. +const INERT_WINDOW_METHODS = new Set(EXTRA_WINDOW_PROPS); + // Ref-count: the pool solves in parallel waves; each window must keep the // aliases alive until the LAST concurrent window is destroyed, otherwise one // destroyDom() pulls `window` out from under a sibling mid-solve. @@ -2124,7 +2130,19 @@ export function removeGlobalWindowAlias(g, w) { if (generation !== _aliasGeneration || _aliasRefCount > 0) return; for (const name of names) { try { - if (Object.getOwnPropertyDescriptor(g, name)?.get) delete g[name]; + if (!Object.getOwnPropertyDescriptor(g, name)?.get) continue; + // Do NOT `delete`: a straggler still reading the name would get a + // ReferenceError, which is fatal in the host realm. Leave an inert + // value instead — the reference resolves, the call is a no-op, and + // nothing keeps the closed window alive. The window methods guest + // fingerprint code probes (moveBy/scrollTo/...) are no-ops in a real + // browser anyway, so `undefined` is a faithful stand-in for the rest. + const stub = INERT_WINDOW_METHODS.has(name) ? () => {} : undefined; + Object.defineProperty(g, name, { + value: stub, + configurable: true, + writable: true, + }); } catch (_) {} } }, _tombstoneMs);