Skip to content

🐛 修复 Firefox 下 @inject-into content 脚本沙盒缺失 EventTarget 方法 - #1692

Open
CodFrm wants to merge 2 commits into
mainfrom
fix/firefox-content-world-sandbox-global
Open

🐛 修复 Firefox 下 @inject-into content 脚本沙盒缺失 EventTarget 方法#1692
CodFrm wants to merge 2 commits into
mainfrom
fix/firefox-content-world-sandbox-global

Conversation

@CodFrm

@CodFrm CodFrm commented Aug 27, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

N/A — 未找到对应的 open issue,问题来自用户直接反馈(Firefox 上运行沉浸式翻译脚本报
TypeError: globalThis.addEventListener is not a function,Chrome 正常)。

背景

在 Firefox 上运行带 @inject-into content 的用户脚本(如沉浸式翻译)时,脚本抛出:

TypeError: globalThis.addEventListener is not a function

同一脚本在 Chrome 正常。因果链:

  1. @inject-into content 命中 isInjectIntoContent()src/app/service/content/utils.ts:284),
    getUserScriptRegister() 把该脚本注册到 world: "USER_SCRIPT"
    src/app/service/service_worker/utils.ts:233-236),而非默认的 MAIN
  2. 该 world 的沙盒由 create_context.tsglobal 为基准收集 PropertyDescriptor 构造,
    global 经 rspack 编译为 __webpack_require__.g,即 globalThis
  3. Chrome 的 USER_SCRIPT world 里 globalThis === window(一个真 Window);
    Firefox 用的是 Cu.SandboxglobalThis 是伪装成 Window 的 Sandbox 对象,
    其原型链在 Xray window 处截断,够不到 EventTarget.prototype
  4. 于是 getAllPropertyDescriptors 永远收集不到 addEventListener /
    removeEventListener / dispatchEvent,沙盒缺失这三个方法。

在真 Firefox 154 的 content world 实测(同一段流水线,两个 world 对照):

MAIN world content world
globalThis === window true false
原型链 Window → Window → WindowProperties → EventTarget → Object Window → Window(2 层到头)
addEventListener 归属 第 3 层 EventTarget.prototype NOT FOUND IN CHAIN
沙盒 addEventListener function undefined

只有 EventTarget.prototype 上那三个方法丢失;setTimeout / fetch / document /
location / alert / localStorage / XMLHttpRequest 均正常(Xray 已把它们平铺为
Sandbox 的自有属性)。

本次改动

create_context.ts 的描述符收集由一趟改为两趟,收集回调参数化,bind 目标跟随当轮根物件:

const collectPropertyDescriptors = (root: any) => getAllPropertyDescriptors(root, (...) => { ... });

collectPropertyDescriptors(global);   // 第一趟:本 realm 完整的 JS 内置物件
collectPropertyDescriptors(window);   // 第二趟:补齐 DOM 原型链
descsCache.clear();

descsCache 原本就是「先到先得(子类覆盖父类)」,天然给出正确优先级:第一趟收下的键不会
被第二趟覆盖。Chrome 下 window === globalThis,第二趟全部命中 descsCache 而跳过,
行为完全不变

diff 中 56/45 行大半是回调缩进位移,实质改动为参数化 + 增加一次调用。

实现考虑

为什么不是把基准直接换成 window 这是首个尝试的方案,实测证明会引入回归:Firefox 的
Sandbox 是独立 realm,其 78 个自有属性几乎全是 JS 内置物件(Array / Number /
Object / JSON / Math / Promise / Symbol / Temporal / WebAssembly …);
而经 Xray 看页面 window 的同名内置物件会被剥到只剩壳:

Xray 看页面 window Sandbox 本 realm
Number 自有属性 length,name,prototype(3 个) 17 个(含 isNaN / isInteger / EPSILON
Math 自有属性 0 个 45 个
String 自有属性 3 个 6 个

一刀切换成 window 后,真扩展验证中沉浸式翻译改抛
TypeError: Number.isNaN is not a function(修复前该错误 0 次,一刀切版本 2 次)。
两趟收集才能两头都对:JS 内置取自本 realm,DOM 原型链取自真实 window。

为什么 bind 目标必须跟随 root。 两个根分属不同 realm,把 EventTarget.prototype
的方法绑到 Sandbox 会触发 brand check 失败;绑到 window 后实测可正常挂监听并被
dispatchEvent 触发。

已知限制

以下两项均已实测修复前后逐字节一致,非本 PR 引入,按 scope discipline 未在此处理:

  1. 沙盒自引用逃逸@inject-into content 的脚本里 globalThis 指向 ScriptCat 沙盒
    typeof globalThis.GM_getValue === "function"),但 window / self 逃逸到页面真实
    window(typeof window.GM_getValue === "undefined")。同一根因的延伸 ——
    create_context.ts 中自引用改写的判定 desc?.value === global 在 Firefox 下不成立
    (Sandbox 的 window 属性值是 Xray window,不等于 globalThis),自引用未被改写为
    返回 mySandbox 的 getter。这属于沙盒隔离缺陷,建议单独开 issue 处理。
  2. 沉浸式翻译在 Firefox 下另有 TypeError: can't access property "call", getter is null
    修复前后均存在;修复后出现次数增加,仅因脚本不再于首个爆点中断、执行到了更多代码。

本 PR 仅覆盖 Firefox 的 content / USER_SCRIPT world;MAIN world 与 Chrome 全部路径
行为不变。

建议审查重点

  • 两趟收集的优先级是否确实由 descsCache 保证(第一趟已收键在第二趟被 descsCache.has(key)
    提前 return)。
  • else if (!(key in initOwnDescs) && !Object.hasOwn(root, key)) 中由 global 改为
    root 是否符合预期:initOwnDescs 仍取自 global,第二趟中 EventTarget.prototype
    的方法两个条件皆成立。
  • Chrome 路径确为 no-op(window === globalThis ⇒ 第二趟全跳过)。
  • 上述「已知限制 1」的自引用逃逸是否应并入本 PR 而非另开。

关联

以下 issue 均标记 FirefoxMV2(本问题在 MV3 路径,strict_min_version 154.0),
未验证是否同源,不作 close 关联,仅供审查时参考:

验证

docs/references/develop-testing.md 的第二条
TDD 例外(「a bug reproducible only in a specific browser version … platform behavior that can't be
automated reliably. Verify it manually and record the evidence」)走手工验证:happy-dom 测试环境下
globalThis === window,该缺陷在单测中无区分力,无法写出会失败的常规单测。

提交的检查

npx tsc --noEmit                        → exit 0
npx vitest run src/app/service/content/ → 11 files / 193 tests passed
npx vitest run                          → 342/343 files, 4277/4278 tests passed
pnpm run lint                           → prettier ✅ / tsc ✅ / check:i18n ✅ / check:issue-templates ✅
npx eslint src/app/service/content/create_context.ts → exit 0

全量中 1 项 UI flaky(src/pages/ 下的 useInstallData / McpFormDialog /
Agent/Tasks / SettingsPane,失败项在多次运行间变化)。已将本改动 git stash 后在
HEAD 上跑同一条全量命令,失败模式完全一致(1 failed / 4277 passed),为仓库既有 flaky;
单独运行这 4 个文件 87 项全过。

Firefox 真扩展验证

pnpm run build 产物 + createFirefoxManifest() 生成解包目录,经 geckodriver
--allow-system-access)安装为临时扩展。userScripts<all_urls> 在 Firefox MV3
下是可选权限、需用户手势,headless 无法点击授权弹窗,故改由 chrome context 调用
ExtensionPermissions.add() 预置(等价于用户在权限弹窗点「允许」)。脚本经
serviceWorker/script/installByCode 种入。构建产物已核对确实包含两趟调用
(tr(h.g),tr(window),e9.clear())。

探针脚本(与沉浸式翻译同为 @inject-into content):

修复前 修复后
结果 typeof=undefined | THREW=TypeError: globalThis.addEventListener is not a function ael=function/fired=1 | builtins_ok=7/7 | Math.max(1,2)=2 | Number.isNaN(NaN)=true

沉浸式翻译真脚本(v1.32.7,installByCode 返回
name: "Immersive Translate: AI Web, PDF & Video Translator"),
devtools.console.stdout.content 统计页面 console:

日志中的错误 修复前 一刀切换成 window 本 PR
addEventListener is not a function 1 0 0
Number.isNaN is not a function 0 2 0

Screenshots / 截图

N/A — 非视觉改动,证据为上述终端输出与浏览器 console 统计。

Firefox 的 content / USER_SCRIPT world 全局是 Cu.Sandbox:原型链在 Xray window 处
截断,够不到 EventTarget.prototype,导致沙盒收集不到 addEventListener /
removeEventListener / dispatchEvent。带 @inject-into content 的脚本调用
globalThis.addEventListener 即抛 TypeError。

改为两趟收集:第一趟仍取 globalThis 保住本 realm 完整的 JS 内置物件(经 Xray 看页面
window 的 Number / Math 会被剥到只剩 length,name,prototype),第二趟取 window 补齐
DOM 原型链。descsCache 先到先得保证优先级;Chrome 下 window === globalThis,第二趟
全部跳过。
@cyfung1031
cyfung1031 self-requested a review August 27, 2026 09:39

@cyfung1031 cyfung1031 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

应该可以。你测试过没问题就merge吧
有问题再修

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