From 8c89c9fb82d795a6c5f0b7671c5abdcd42fa5b2d Mon Sep 17 00:00:00 2001 From: WASD1999xl <2960525002@qq.com> Date: Sun, 6 Sep 2026 15:48:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=E8=A1=A5=E4=B8=8A=20rAF=20?= =?UTF-8?q?=E5=88=AB=E5=90=8D=EF=BC=8C=E5=B9=B6=E7=BB=99=20getter=20?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=E7=9A=84=20rejected=20promise=20=E5=8A=A0=20?= =?UTF-8?q?sink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处独立的求解器缺口,都只表现为静默劣化,不产生可见报错。 1) requestAnimationFrame / cancelAnimationFrame 移出 HOST_CRITICAL_GLOBALS 这个跳过表的用意是"别遮蔽宿主依赖的内建",但 Bun 根本没有原生 rAF,所以跳过 别名等于让它对 guest 不可解析。feilin008 里有 9 处裸引用,只有 1 处带 typeof 保护,其余落在 Promise executor 内——因此它不会崩,只会把指纹链打断,表现为 求解成功率下降。 与 2026-08-29 修 `print` 的那次是同型问题(同样的跳过表、同样的静默劣化)。 Bun 的宿主全局没有 rAF,别名到 window 的实现不遮蔽任何东西。 2) installNativeToString 探测 getter 时产生的 rejected promise 加 sink maskObj 会调用 getter 以便给返回的函数也套上 native toString 伪装。WHATWG stream 的 `closed`/`ready` 在接收者是原型而非实例时返回一个已拒绝的 promise,没人 await,于是每次求解稳定产出 4 条 unhandledRejection: The 'closed' getter can only be used on a ReadableStreamBYOBReader The 'ready' getter can only be used on a WritableStreamDefaultWriter 探测结果本来就是丢弃的,加一个 .catch(() => {}) 即可。这类噪音会淹没真正需要 关注的诊断信息。 验证:别名安装后 bare requestAnimationFrame 解析为 function;完整一轮求解的 unhandledRejection 从 4 条 stream getter 噪音降为 0 条。 --- src/proxy/captcha-happy.test.ts | 22 ++++++++++++++++++++++ src/proxy/captcha-happy.ts | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/proxy/captcha-happy.test.ts b/src/proxy/captcha-happy.test.ts index be615d9..a5acc4a 100644 --- a/src/proxy/captcha-happy.test.ts +++ b/src/proxy/captcha-happy.test.ts @@ -251,3 +251,25 @@ describe("alias lifecycle (install/remove descriptor contract)", () => { expect(Object.getOwnPropertyDescriptor(g, "setTimeout")?.get).toBeUndefined(); }); }); + +describe("browser globals the guest SDK dereferences bare", () => { + test("requestAnimationFrame/cancelAnimationFrame resolve during a solve", () => { + // Bun has no native rAF. While these sat in HOST_CRITICAL_GLOBALS the + // alias pass skipped them, so the FeiLin bundle's 9 bare + // `requestAnimationFrame` references (only one `typeof`-guarded) were + // unresolvable — the same silent fingerprint degradation `print` caused, + // visible only as a lower solve rate. + const g: Record = {}; + const w: Record = { + requestAnimationFrame: (cb: () => void) => 1, + cancelAnimationFrame: () => {}, + }; + installGlobalWindowAlias(g, w, 15); + try { + expect(typeof g.requestAnimationFrame).toBe("function"); + expect(typeof g.cancelAnimationFrame).toBe("function"); + } finally { + removeGlobalWindowAlias(g, w); + } + }); +}); diff --git a/src/proxy/captcha-happy.ts b/src/proxy/captcha-happy.ts index c82f300..926fcce 100644 --- a/src/proxy/captcha-happy.ts +++ b/src/proxy/captcha-happy.ts @@ -630,7 +630,13 @@ function installNativeToString(w) { try { const v = desc.get.call(obj); if (typeof v === "function") mask(v); - } catch (_) {} + // Probing a getter can hand back an already-rejected promise (WHATWG + // stream `closed`/`ready` reject when the receiver is the prototype + // rather than an instance). Nobody awaits these, so without a sink + // each probe surfaced as an unhandledRejection on every solve — + // noise that buried real diagnostics. + else if (v && typeof v.then === "function") v.catch(() => {}); + } catch {} } if (depth < 3) { try { @@ -1774,7 +1780,12 @@ async function createDom(region, prefix) { const HOST_CRITICAL_GLOBALS = new Set([ "process", "Bun", "console", "performance", "crypto", "fetch", "queueMicrotask", "structuredClone", "TextEncoder", "TextDecoder", - "requestAnimationFrame", "cancelAnimationFrame", + // NOTE: requestAnimationFrame/cancelAnimationFrame were removed from this + // list (2026-09-06). Bun has no native rAF, so skipping the alias left a + // bare `requestAnimationFrame` in the FeiLin bundle unresolvable (9 call + // sites, only one `typeof`-guarded) — the same silent fingerprint + // degradation that `print` caused. Aliasing the window's implementation + // shadows nothing on the host. // NOTE: `print` was removed from this list (2026-08-29). The polyfill // defines a harmless no-op on the window, but the alias pass skipped it, // so under Bun (guest scripts run in the HOST realm) the Aliyun pe risk