漏洞
超时铃响后 Promise 才来取件——Chromium reportWin UAF 复盘(CVE-2026-15133)
CVE-2026-15133 的故事始于一场已经结束的 Protected Audience 竞价:买方上报脚本把 browserSignals 留在 globalThis ,又排入稍后读取 renderUrl 的 Promise reaction,最后用死循环撞上超时,于是 C++ 助手先随栈离场,context 回收器随后排空微任务队列,那张迟到的取件票重新打开由失效栈对象支撑的抽屉;补丁则让 filler 留到 scope 清理完毕,把 logger 借用限制在脚本执行阶段,并在恢复延迟 JavaScript 前收起其他原生连接。

文章导航
1 赢下竞价以后,上报室里多了一只不会立刻打开的抽屉
1.1 browserSignals 信封同时装着普通字段与一枚旧拼写门把
故事从一场已经分出胜负的广告竞价开始。Chromium 的 Protected Audience 实现会把买方和卖方 JavaScript 放进 auction worklet;当某个买方胜出,BidderWorklet::V8State::ReportWin() 负责准备参数并调用它的 reportWin()。这一步不再决定谁赢,它让买方登记竞价后的报告 URL、beacon、private aggregation 请求和其他获准输出。
浏览器递给脚本的参数里有一只 browserSignals 信封。里面装着浏览器知道、页面脚本不能自行声明的竞价事实,例如最终出价与渲染地址。对脚本作者而言,它看起来只是一个 JavaScript 对象;对 Chromium 而言,其中不同字段可能通过不同的 C++ 路径准备,取值时机也不完全相同。渲染地址就有两种大小写不同的拼写:推荐字段 renderURL 在构造对象时同步写入,是一张已经放进信封的普通卡片;旧兼容字段 renderUrl 则通过 DeprecatedUrlLazyFiller::AddDeprecatedUrlGetter() 安装成延迟 getter,像信封旁一只只在有人拉动时才打开的黄铜抽屉。
这项差别决定了整条漏洞路径。读取 renderURL 不需要找回栈上的 deprecated filler;读取 renderUrl 会进入原生属性回调。Promise、超时和栈回退都围绕旧别名的 getter 展开,两个字段不能被并入同一条执行链。
延迟填充原本有很实际的目的。宿主无需在创建每个对象时立刻计算所有属性,也能只在脚本真正访问旧字段时发出一次弃用警告。DeprecatedUrlLazyFiller 因而保存 URL、警告内容和一条通往 AuctionV8Logger 的连接,再让 V8 getter 的 callback data 能够找到这个 helper。正常的同步读取很安静:脚本在 reportWin() 调用期间拉开抽屉,filler 仍在栈上,logger 也仍属于当前 V8 context;回调先写入一条弃用提示,再把 URL 转成 V8 字符串。旧代码长期表现正常,正因为最常见的访问发生在这些对象共同存活的时间段。
危险场景没有要求 getter 在脚本顶层立即运行。JavaScript 可以把对象保存到 globalThis,也可以把一次属性读取交给 Promise reaction。此时信封仍由 V8 保管,取件票进入微任务队列,而负责开抽屉的 C++ helper 依旧只是一次原生函数调用里的局部对象。
从网页侧看,保存一个对象、排入 Promise、让函数运行过久,都是普通语言动作。Chrome CNA 对 CVE-2026-15133 的产品级描述也明确把入口写成 crafted HTML page。漏洞根因不在某个私有广告后台接口,它发生在浏览器把网页可控脚本与原生 worklet 宿主连接起来的地方。
不过,能写 HTML 并不代表每个浏览器标签页都会自然经过这间上报室。环境仍要运行相应的 Protected Audience 竞价与买方上报,脚本还要到达旧属性的延迟读取和超时清理组合。资产研判需要确认功能、进程与构建,不能只按网页访问量推导暴露程度。
先把桌上的四件东西记住:蓝色信封是仍可达的 browserSignals,黄铜抽屉是 renderUrl getter,取件票是排队的 Promise reaction,红铃是上报时限。后面每一章只改变它们的相对时刻,直到补丁让迟到的票据再也找不到失效助手。
买方的 reportWin() 也要与卖方的 reportResult() 分清。两者都属于竞价后脚本,也会使用 auction-worklet 基础设施;公开回归和直接 UAF 修复落在 bidder 路径。seller 文件出现在同一 diff,是维护者顺手清理另一个 lazy-filler 构造接口,不能据此把卖方上报写成已经触发本 CVE 的第二入口。
信封内的字段也并非都由 filler 负责。固定值可以直接写入 V8 object,按需字段才登记 native getter。审阅时应从 SetBrowserSignals() 的具体赋值语句追踪每个拼写,不能看到同一个对象名便假设它们共享回调、缓存与存活规则。
弃用 getter 的“首次访问”语义还解释了为什么大量页面不会留下警告:若 bidder 从不读 renderUrl,回调就不需要运行。Promise reaction 专门在清理阶段做第一次读取,恰好把延迟计算的优势变成一次迟到的 native 入口。
这类属性桥保存的是生成值的方法,不是一份已经复制好的 URL。若 getter metadata 只携带独立字符串,栈对象离场不会产生同样后果;源码显示它携带 helper 地址,所以对象拥有关系和执行时间才成为主线问题。
1.2 JavaScript、filler 与 logger 使用三只不同的时钟
第一只时钟属于 V8。只要 browserSignals 仍被全局变量或闭包引用,JavaScript 对象就可以继续存活;只要 Promise reaction 还留在 microtask queue,它就可能在下一次 checkpoint 恢复。垃圾回收器只理解 JavaScript 可达性,并不知道 callback data 指向 C++ 栈上的哪一帧。第二只时钟属于 DeprecatedUrlLazyFiller:受影响代码把它放在 ReportWin() 的当前栈帧,函数退出时便依照 C++ 规则结束生命周期;V8 元数据保存一段地址不会改变声明顺序,也不会让编译器自动延后析构。
第三只时钟属于 AuctionV8Logger。旧 filler 从构造开始便借用 logger,用它向当前 V8 context 写弃用警告;同步调用期间这条借用成立,进入失败收尾后则需要重新判断。logger 的名字听起来像附属设施,实际调用仍依赖完整的宿主状态。
ContextRecycler 与 ContextRecyclerScope 决定第四个关键时刻。scope 在构造时取得本轮短生命周期 context,在析构时调用 ResetForReuse();reset 不只释放数据,还可能主动执行 JavaScript。一个看似只负责收尾的 RAII 对象,因而掌握了本轮最后一次回调机会。
成功路径会掩盖四者的差异。脚本若正常返回,排队 reaction 通常在局部对象仍齐全时运行,旧 getter 也能写完警告。只有超时让自动 checkpoint 缺席,队列继续等待,而 C++ 栈仍照常进入退出程序。
这条链不需要两个操作系统线程争抢同一指针。它可以在同一 isolate、同一原生调用栈上稳定发生:V8 先进入 terminating 状态,C++ 随后逆序销毁局部对象,scope 最后通过 recycler 补做 checkpoint。根因是两个运行时对“调用是否已经结束”采用了不同标尺。
单画 JavaScript 对象图,只会看见仍然可达的信封;单看 C++ 栈图,又会看见按规则离场的 helper。把两张图叠起来,矛盾才出现:getter metadata 仍能找到原地址,地址中的 C++ 对象却已经结束生命周期。每一侧都遵守自己的规则,跨语言连接没有携带共同的有效期。
因此,给 logger 增加判空并不足以独立修复。回调必须先从 V8 External 取回 filler 的 this,随后才可能读取 self->v8_logger_;若 filler 已失效,连执行判空都已经太晚。直接修复必须先延长 filler,再限定 logger 的借用区间。
V8 External 在这份清单里只属于“保存地址”一列。它没有引用计数、unique ownership 或析构协作,把栈地址塞进受 GC 管理的对象也不会把对象迁移到堆上。这个区别解释了为何 JavaScript 信封活着,柜台后的助手仍然可以离场。
auction worklet 的进程布局又增加了一层距离:页面发起竞价,bidder JavaScript 和 logger 状态却在承载 worklet context 的服务进程中运行。事故采集若只拿外层标签页或安装器日志,可能完全错过 getter 与 checkpoint 所在的真实进程。
还要把 URL 指向的数据与保存该引用的 filler 分开。公开源码已经足以证明回调进入生命周期已结束的 filler,并继续读取其成员;无需猜测底层 GURL 存储是否同时被覆盖。把证据停在第一处无效对象访问,结论反而更精确。
把这一轮调用放进同一张对象账本,错位就很清楚:browserSignals 由 V8 context 承载,Promise reaction 由微任务队列保留;栈上的 deprecated_render_url 通过 callback data 暴露地址,v8_logger 又只是它借用的短命资源;并不被 getter 引用的 ContextRecyclerScope,却能在析构时触发最后一次 JavaScript。前两者由可达性与 checkpoint 决定,后两者由声明顺序与显式状态决定,而 scope 掌握真正的最后调用点。
2 Promise 留下一张取件票,超时只赶走了柜台后的工作人员
2.1 上游回归把四个动作排成一条可重复的窄路
新增测试名为 BidderWorkletTest.ReportWinBrowserSignalRenderUrlDeprecationAsync。它没有构造庞大竞价,也没有依赖模糊随机时序;测试体先保存参数,再排一个 Promise,最后进入无限循环。四个动作分别固定对象可达性、延迟属性访问、旧 getter 和超时。
第一句把 browserSignals 放进 globalThis.savedBrowserSignals。这一步保证 reportWin 的形参作用域结束后,对象仍由全局引用保住。它验证的是 JavaScript 信封可以留在房间里,并未声称这项写法是现实攻击的唯一储存方式。
第二步调用 Promise.resolve().then(...)。reaction 里只有一次 void globalThis.savedBrowserSignals.renderUrl,返回值本身没有被利用。测试关心的是属性访问会启动 native lazy getter,void 让其他输出不来干扰结果。
第三步进入 while (true) {}。reporting time limit 最终要求 V8 终止这次执行,worklet 把 reportWin 标记为超时。无限循环在这里是一台稳定触发计时器的实验装置,不能被理解为所有现实路径都必须长成相同源码。
测试通过 CreateReportWinScript(kBody) 把这段 body 放进 reportWin(),所以死循环发生在函数调用阶段,不是顶层 worklet script 加载阶段。这个位置会进入补丁中第二个 detach 出口,也解释了精确错误为何写成 execution of `reportWin` timed out。同一固定测试文件另有 ReportWinTopLevelTimeout,它把 while (1) {} 直接作为顶层脚本,并期望精确错误 https://url.test/ top-level execution timed out.。两项测试分别覆盖 RunScript() 与后续函数调用的失败分支;错误文字相近,控制流和输出处理却不能互换。
第四步没有写在 JavaScript 里,它由宿主清理自动完成。Promise reaction 已排队却尚未执行,原生控制流处理 timeout,局部对象开始离场,最后 scope 析构触发 recycler checkpoint。取件票于是从 JavaScript 队列重新进入 C++ getter。
这个 fixture 没有在死循环前登记任何上报输出,helper 因而精确期望:report URL 为 null,ad beacon 与 ad macro map 为空,private aggregation 请求为空,private model-training 数据也为 null。这五项断言证明用例没有从别的路径混入意外结果,不代表所有超时都会丢弃先前已经产生的输出。它还要求 reporting-latency timeout 标志为真,并只收到一条精确错误:https://url.test/ execution of `reportWin` timed out.。因此测试不是一条仅以“进程没死”为成功条件的模糊用例,它还锁定了上层可观察语义。
公开测试没有包含 EXPECT_NO_CRASH 字样,也没有在这一个用例里断言警告文本或 getter 返回的 URL。修复后的完整返回与精确结果让测试通过;相邻同步测试继续负责弃用警告兼容性,不能给这个 TEST_F 添加源码中不存在的断言。
Promise.resolve() 本身已经 fulfilled,仍不会在当前 JavaScript 语句中同步调用 then handler。ECMAScript 的微任务语义把 reaction 排入队列,这正好给测试留下一个明确的延迟点。若改成普通同步属性读取,handler 会在死循环前完成,根因不会被覆盖。全局保存对象还有第二个作用:它让 reaction 不必依赖 reportWin 形参的词法作用域;即使函数因 timeout 非正常离开,context 中仍存在一条稳定路径找到同一个 V8 object,recycler 不能只清理 C++ 调用参数便假定对象已经不可达。
精确错误里的 https://url.test/ 是测试 fixture 的脚本来源,用于验证错误归属。生产调查不应把该域名当 IOC,也不能要求现实异常出现相同字符串;真正有意义的是错误类别、timeout 状态和对应 bidder script provenance。
这里尤其要区分 Private Aggregation:固定源码在 timeout 分支特意保留脚本终止前已经产生的相关请求。新增 fixture 之所以期望该集合为空,只因测试脚本从未创建请求;它锁定的是这段脚本的结果,不是“超时后一律清空”的平台规则。
2.2 V8 正在终止执行时,常规微任务检查会被跳过
正常 JavaScript 调用回到宿主时,V8 会在合适的策略点执行 microtask checkpoint,把已经就绪的 Promise reactions 逐个取出;若 reportWin() 顺利结束,排在队列里的 getter 很可能仍处在 filler 和 logger 可用的脚本执行阶段。超时终止却属于特殊状态。回归测试的源代码注释明确写道:V8 正在 terminating 时会跳过正常的自动 microtask checkpoint。无限循环被终止以后,Promise 没有消失,也没有当场执行,它只是继续躺在 isolate 的队列里等待下一次宿主检查。
这一点让“脚本已经失败”和“脚本排下的工作已经清空”成为两件事。worklet 可以正确报告 timeout,原生函数也可以开始整理输出,但仍有一段 JavaScript reaction 保留在同一个 context 中。控制流随后撤销终止状态、处理脚本结果并沿 C++ 栈退出;对外层调用者而言,reportWin 已经失败,对微任务队列而言,取件票尚未作废。两种状态并存,正是 ContextRecyclerScope 稍后执行 checkpoint 时仍有 JavaScript 可跑的原因,也要求主动 drain 队列的清理代码按可重入路径处理每个宿主回调。
旧实现需要手工 checkpoint,是因为 timeout 路径不能把排队反应遗留到 context 的下一位使用者。ResetForReuse() 的注释强调要像正常返回那样 flush microtasks。设计目标本身合理,问题出在它执行时,某些只属于本轮调用的栈对象已经先行析构。
这条链不依赖调度器随机挑选一个窗口。测试通过明确定义的终止状态和 scope 析构建立顺序:自动 checkpoint 被跳过,局部对象逆序销毁,手工 checkpoint 再运行。实现细节若改变,执行点可能移动,但修复要保证无论队列在哪里恢复,回调背后的 native 状态都有效或已安全断开。
源码评审可把 terminating 状态当成一面红旗:任何在恢复后手工运行 microtask、finalizer 或回调队列的代码,都要重新检查此前析构了哪些 borrowed object。这里的危险并非“微任务不该运行”,真正需要修正的是宿主为微任务留下了过期 callback data。
checkpoint 处理的是整条就绪队列,不会只挑新增测试里的目标 reaction。真实 context 还可能恢复其他 Promise handler,所以补丁把 recycler-owned bindings 与 fillers 全部 reset 后才 drain;这是对清理阶段可重入性的系统准备,栈上 deprecated filler 则由声明顺序单独保护。
AuctionV8Helper::TimeLimitScope 仍然包住手工 checkpoint,说明维护者没有用“延长清理时间”换取生命周期安全。延迟 JavaScript 依旧受资源限制;变化只在于它于限额内重入时,能看到的 native 连接已经撤销或仍然有效。
3 C++ 按构造的逆序关灯,最晚离场的 scope 却会再唤醒 JavaScript
3.1 旧 ReportWin 把最该活到最后的 filler 声明在了靠后位置
修复前的 ReportWin() 先进入完整 isolate scope,然后创建 ContextRecycler 与 ContextRecyclerScope;scope 取得一个 V8 context,并承诺在析构时通过 recycler 清理,负责最终关门的对象至此已经较早出现在栈上。函数随后创建栈上的 AuctionV8Logger,继续构造 browser signals 和其他调用参数。直到渲染 URL 已经准备好,代码才声明 DeprecatedUrlLazyFiller,把 helper、logger、URL 与弃用警告连在一起,再为 renderUrl 安装 getter。
C++ 局部对象在离开作用域时按完成构造的逆序析构。这是由代码声明次序导出的语言语义,不需要等待崩溃日志确认。后来创建的 deprecated filler 会先结束,logger 随后结束,较早创建的 ContextRecyclerScope 才进入自己的析构函数。
正常返回掩住了这个次序。getter 通常在脚本仍运行时完成访问,等函数退出时,V8 已经没有 reaction 会再找 filler。于是“filler 先死、scope 后死”看起来无害,直到 timeout 把一个 getter 调用留给 scope 的析构工作。
旧 filler 构造函数保存 AuctionV8Logger*、const GURL* 和警告字符串指针。它们都是借用,依赖调用者安排存活时间。同步路径满足契约;超时路径让 filler 自身先结束,因此无论所借对象中哪一个仍在,回调都已经没有合法的 this。
公开提交的父对象 ce8e75161d0388fa68ebc01a48cbdaa04d863d94 只提供修复前快照。它的提交主题是无关的 Glic 改动,不能把它写成漏洞引入点;公开资料也没有给出第一个受影响版本。2026 年 6 月 26 日只说明补丁在这一天合入 main,判断旧分支是否受影响仍需对照该分支源码或厂商 affected range,不能从相邻 Git parent 反推回归起源,也不能把“主线修复前所有历史版本”直接变成产品结论。
现在,filler 与 logger 已按逆序离场,桌上只剩较早创建的 scope。下一步不是简单释放 context;ContextRecyclerScope::~ContextRecyclerScope() 会调用 ResetForReuse(),而旧函数的第一件事就是转动仍装着 Promise 票据的微任务托盘。
ContextRecycler 与 ContextRecyclerScope 虽然名称相近,存活职责并不相同。recycler 拥有本轮 context 及其 bindings,scope 代表一次进入和退出;本案中真正触发 checkpoint 的是 scope destructor,不能把调用笼统写成“recycler 自己稍后运行”。
对象析构与存储清零也不是同一动作。编译器没有义务把栈槽抹成零,旧 pointer 数值可能保持原样;因此一次调试运行仍能输出警告或 URL,下一次却崩溃。以“内存看起来还像原对象”为依据继续解引用,正是生命周期漏洞的典型陷阱。
3.2 ResetForReuse 先转动微任务托盘,随后才收起可复用连接
ContextRecyclerScope 的析构函数很短,核心就是 context_recycler_->ResetForReuse(),但短函数并不等于无副作用:reset 需要把一个执行过不可信脚本的 V8 context 整理到可安全退出的状态,期间会触碰 bindings、lazy fillers 与 microtask queue。受影响版本的 ResetForReuse() 首先创建 AuctionV8Helper::TimeLimitScope,随后调用 isolate()->PerformMicrotaskCheckpoint();在回归场景中,这正是 V8 终止时漏掉的那次 checkpoint,队列里的 reaction 会在这里执行。
checkpoint 之后,旧函数才遍历 bindings_list_ 调用 Reset(),再依次重置 bidding browser signals、interest group、seller browser signals、report-win browser signals 与所有 auction-config lazy fillers,因此微任务运行时,刚结束调用的其他宿主连接也尚未统一收起。本案的 deprecated URL filler 不在这份 recycler-owned reset 列表里;它是 ReportWin() 的栈对象,直接问题来自它在 scope 之前析构。两者不能混为一谈:移动 reset 次序并不会删除 renderUrl getter。
真正发生的重入是:scope 开始析构,reset 执行 checkpoint,Promise reaction 读取仍存活的 JavaScript 对象,V8 找到它先前登记的 getter callback data,callback data 又给出已经结束生命周期的 filler 地址。队列和对象都有效,失效的是两者背后的 native 支撑。这一过程没有另一个线程插入,也无需堆回收恰好发生;栈内存可能尚未被新的局部变量覆盖,某些运行甚至会继续返回看似正确的 URL,但内存内容“还在”不等于 C++ 对象依旧存在。
这也解释了为什么普通 release 构建可能表现得不稳定,sanitizer 或 debug 配置却更容易暴露问题。栈槽复用、编译优化、插桩和日志路径会改变后果,却不会改变回调进入失效对象这一事实;修复评审不能以某次未崩溃运行作为否定证据。提交说明称其他 lazy filler 在代码通读中看起来正常,但仍将 checkpoint 移到 reset 之后以加强保险;这是公开审阅结论,不是所有未来回调的形式化证明,固定 diff 只证实本案栈上 filler 的直接 UAF 和一项更广的防御性加固。
对复用型执行环境而言,顺序原则很朴素:先撤销本轮 invocation 暴露给脚本的 native 连接,再允许任何延迟 JavaScript 运行,最后把 context 交给下一轮。补丁实现了这项顺序,却还需要另一半直接修复,让 deprecated filler 在 checkpoint 期间保持有效。reset 列表的内部排列没有改写:bindings 先整体 reset,随后各类 signals filler 依条件存在逐一 reset,最后遍历 auction-config fillers;补丁只是把完整区块搬到 checkpoint 之前,从而降低行为漂移。
ReportWin() 旁的固定源码注释把这个 context 定义为 short lived,目的正是防止全局状态泄露到同一 worklet 的重复调用或其他 worklet;此处的 ContextRecycler 是函数局部对象,ResetForReuse() 沿用类的通用命名,本案不应据此描绘一个跨多轮长期共享的 context。手工 checkpoint 后,队列应达到宿主预期的清空状态,scope 才能完成退出;若某个 reaction 又排入后续工作,具体 V8 policy 决定是否在同一 checkpoint 继续运行,原生状态必须对整个 drain 过程安全,不能只覆盖第一个 callback。
这也是为什么修复把 filler 放到 recycler 之前,而非仅挪到 logger 之前:scope、recycler 以及它们可能触发的全部清理都要结束后,helper 才能离场,声明位置一次性表达了整段覆盖区间。即使 context 会在函数结束后随 recycler 一同销毁,scope teardown 仍要为了 flush 队列执行 checkpoint,栈上 getter 仍需要有效 owner;直接 UAF 完整发生在本轮调用收尾之内,不依赖后继竞价取得同一 context。源码中的 TimeLimitScope 也必须留在等价回移里;若为修复 UAF 直接删除 checkpoint 或时限包装,可能留下未清微任务或清理阶段无限执行,安全存活与原有资源控制需要同时成立。
4 信封还活着,抽屉门把背后的栈对象已经不在
4.1 V8 External 保存地址,却不会替 C++ 延长对象生命周期
LazyFiller::DefineLazyAttribute() 通过 SetLazyDataProperty() 登记 lazy property 与 handler,info.Data() 携带指向 filler 本体的 v8::External(this) 和 External type tag。注册过程没有复制 URL、warning 或 logger,也没有创建 C++ owner;它只解决“以后找回哪个类型的 helper”,不回答“那时实例是否仍然存在”。属性首次读取时,HandleDeprecatedUrl() 调用 GetSelf<DeprecatedUrlLazyFiller>(info),以 kTag 取回 External 的值。tag 能阻止把别类 filler 静默解释成当前类型,却不保存 C++ 生命周期或对象世代;本案静态类型正确,失效的是实例。
旧 handler 接下来无条件执行 self->v8_logger_->LogConsoleWarning(self->warning_.get())。为了读取 v8_logger_ 成员,程序首先要把 self 当成有效 filler;此时 filler 已析构,所以危险在 logger 调用之前就已经成立。logger 也已经按旧声明顺序离场,随后 handler 还要取得 self->v8_helper(),读取 self->url_ 指向的 URL,并转成 V8 值;即便某个 pointee 恰好仍在,访问保存这些指针的失效 filler 依旧非法。
AddDeprecatedUrlGetter() 在 url_ 为空时直接返回成功,不会登记 lazy property;只有 URL 存在时才调用 DefineLazyAttribute()。handler 的正常结尾又区分转换成功与失败:gin::TryConvertToV8() 成功便返回 URL 字符串,失败则返回 V8 null。因此,这不是一条“弃用警告偶尔写错”的兼容性缺陷:警告路径只是第一个显眼的 native 解引用,同一 getter 还承担 URL 返回。Chromium 提交标题直接写明修复 reportWin deprecatedUrl handler 的 lifetime,Chrome 公告将产品问题归类为 InterestGroups use-after-free。
修复不能简单让 V8 External 拥有这个栈对象,因为 helper 的其他成员仍借用每次调用的数据,把它改成任意长寿命会带来新契约。上游选择的方案更精确:让 filler 只多活到 scope reset 结束,同时只在脚本正常执行区间挂接短命 logger。
这张地址链也给审计者一个可复用问题:任何把栈对象地址塞进 V8、JS、回调队列或异步 token 的代码,都要列出最后一次回调可能发生的位置,再证明 native 对象覆盖该位置。类型安全 External 可以防止拿错类型,不能证明拿到的是仍存活的实例。
LazyFiller 头文件把这项时序写成类级契约:为避免 UAF,与 filler 关联的 v8::Context 必须在 filler 之后立即销毁;更理想的排列是 context 先销毁、filler 后销毁。之所以保留前一种许可,是某些 subclass 还会借用 context-scoped 变量,这些变量又必须活过 filler,注释把 AuctionV8Logger 列为例子。“立即”在这里具有控制流含义:若 filler 结束后,关联 context 仍能经过另一段局部对象清理再运行 getter,External 中的地址便有重新被使用的窗口;旧 ReportWin() 恰好先析构 filler、再析构 logger、最后由 scope reset 运行 checkpoint,既没有做到 context 先结束,也没有让两者紧邻退出。
补丁采用头文件所称的更理想排列:deprecated filler 先于 recycler 与 scope 构造,逆序退出时,与 context 相关的 scope 清理和 checkpoint 都在 filler 仍存活时完成。这个通用契约解释了为何移动声明是直接修复,却不能被延伸成其他 subclass 已经存在可触发漏洞的证明。
修复后的 null logger 也不是从失效对象中“安全读出零”。它之所以可读,是因为 filler 已经通过声明顺序保持存活,detach 再把成员设置为 null。先保证对象有效,再检查成员状态,这个先后关系必须在说明和回移中保持。
4.2 Chrome 将它定为 High:crafted HTML 可在沙箱内执行任意代码
Google 在 2026 年 7 月 8 日桌面 Stable 公告中把 CVE-2026-15133 列为 High,类型为 InterestGroups use-after-free,并关联 bug 527406824。公告还记录 Jihyeon Jeong 于 6 月 24 日报告、奖励 500 美元;这些字段用于识别同一案件,不能代替影响分析。
Chrome CNA 的正式描述给出产品级结论:Google Chrome 早于归一化版本 150.0.7871.115 时,远程攻击者可借 crafted HTML page 在 sandbox 内执行任意代码。“inside a sandbox”是句子的一部分,事件记录与翻译都必须保留。
公开资料没有证明沙箱逃逸、跨源数据读取、持久化、账户接管或某个具体广告平台遭到入侵,Stable 公告也没有宣布野外利用。缺少这些证据时,事件记录应停在厂商已经确认的沙箱内代码执行,不把可能的后续影响写成已经发生的事实。
CVE 记录中 CISA ADP 的 SSVC 字段把 exploitation 标为 none,这只是该容器在记录更新时间点的判断,未来研判应重新查证,不能把它保存成永久结论。NVD 页面展示 CVSS 3.1 8.8 High,但提供者同样标注为 CISA-ADP secondary,并非 Chrome CNA 自行发布的分数。对外通告可同时列出 Google High 和这项二级评分,只要清楚写明来源,避免把不同机构的判断拼成一句“Google 评分 8.8”。
从栈上 UAF 走到稳定代码执行,还涉及编译配置、栈槽复用、控制流加固、进程模型与可控数据。CNA 已经给出足够推动修补的产品结论;公开材料没有提供可据以描述的内存布局、利用稳定化步骤或成功率。
worklet 运行在浏览器受限进程和权限模型之中,sandbox 内任意代码执行仍会破坏该进程应有的控制流与数据完整性,所以优先级不能因隔离存在而降为普通脚本错误。若调查同时发现宿主层或跨进程后果,必须为新增后果建立独立证据链。
本地优先级要再叠加可达性:实际 Chromium revision、Protected Audience 是否编入并启用、第三方 bidder 来源、worklet 进程常驻时间和升级频率。广告测试农场、kiosk 与自建 embedder 可能比日常桌面更频繁运行上报路径,清点时应单列。
“remote attacker”证明内容可经网页交付,不证明每个打开网页的进程都会进入 reportWin。资产表可以把状态写成“含脆弱代码”“功能可达”“出现相符异常”三档;三档都应升级,只有后两档需要额外的功能和事件证据。
5 补丁一手延长 filler,一手缩短 logger 的借用时间
5.1 helper 被移到 scope 之前,logger 只在脚本工作区间接通
主线提交 5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13 首先改变 ReportWin() 的声明顺序。取得 isolate 后,代码立即构造 DeprecatedUrlLazyFiller deprecated_render_url,随后才创建 ContextRecycler 与 ContextRecyclerScope。新位置旁的注释直接写明 filler 需要活过 ContextRecyclerScope;局部对象逆序析构后,scope 会先完成 ResetForReuse() 和 checkpoint,recycler 再结束,较早构造的 filler 最后才析构。迟到 reaction 进入 getter 时,this 仍是有效对象。
单独延长 filler 还不够,因为旧构造函数会从一开始就保存栈上 logger。补丁删除 logger 构造参数,让 v8_logger_ 初始为 null;URL 和警告仍由 filler 保存,日志连接则从永久借用改成可显式开关的状态。代码在参数数组已经准备完成后才调用 deprecated_render_url.SetLogger(&v8_logger),下一步依次是 reporting instrumentation breakpoint 与顶层 RunScript()。此前的 signals 转换或参数构造若失败,成员始终为 null;从断点恢复到后续函数调用,logger 才明确处于可用区间。
attach 后的第一个出口来自顶层 worklet script 运行失败。固定 diff 先 detach,再结束 trace 并回传 null report URL、空 beacon、空 macro、空 PA 和 null PMT;script_timed_out 直接由 RunScript() 是否返回 timeout 决定,此时 reportWin() 尚未被调用。第二个出口覆盖 reportWin() 或 reportAdditionalBidWin() 调用失败与超时。代码同样先 detach,然后把 report URL、beacon 与 macro 置空,却通过 private aggregation bindings 取走已经产生的请求;源码注释说明脚本可能用 PA 检测 timeout 或 failure。两条失败分支的输出语义不同,logger 的清理次序相同。
实际调用名由 is_for_additional_bid 选择:普通胜出进入 reportWin,additional-bid 路径进入 reportAdditionalBidWin,两者接收同一组已准备参数,也共用 deprecated getter、logger 状态与 timeout 清理。公开新增 TEST_F 直接覆盖前者;后者纳入相同修复控制流,是固定源码可见的覆盖,不能写成已经有同名异步回归。
成功路径还可能处理 queueReportAggregateWin 留下的 modeling-signals 配置。只有 modelingSignals、joinCount 与 recency 未在 reportWin() 中被读取,并且 payload length 不超过上限时,代码才开放相应 Private Model Training binding;随后准备参数并可选调用 reportAggregateWin。在 binding 已建立并调用这项后续函数的分支,源码为了避免泄露一位状态,即使 sendEncryptedTo() 未调用、函数报错或超时,也会生成带空 payload 的请求;payload 超过声明长度同样清空并记录错误。这组产品语义说明为什么成功收尾不能在 reportWin() 返回后立刻截断所有 output binding。
等这些可选步骤全部完成,固定源码才 detach;旁边注释同时覆盖“提供了 report URL”和“没有提供 URL”两种成功结果。代码随后统一取走 report、beacon、macro、PA 与可选 PMT 输出并回传,logger 的借用区间因此覆盖整段 reporting 工作,却不会跨进 scope teardown。getter 本身也增加条件检查:只有 v8_logger_ 非空时才调用 LogConsoleWarning();清理 checkpoint 中的迟到读取仍能从活着的 filler 返回 URL,却不会进入已经结束的 logger,同步旧字段访问则保持原有警告行为。
DeprecatedUrlLazyFiller 析构函数新增 DCHECK_EQ(v8_logger_, nullptr)。它让 debug 与测试构建在新增出口遗漏 detach 时尽早失败,但不会出现在 release 保护路径;生产安全仍由对象顺序、显式 detach 和 getter guard 共同实现。
修复后的 deprecated_url_lazy_filler.h 还把数据契约写得很具体:构造函数接收的指针都必须活过 filler,url 与 warning 在 filler 销毁前不得修改。这解释了为何补丁选择调整局部对象顺序,没有把 URL 或警告的借用默默改成另一种所有权模型。SetLogger() 则允许两种安全状态:logger 活得比 filler 久,或在 logger 离场前把成员清空;ReportWin() 采用第二种,因为 per-call logger 没有必要覆盖 context teardown,显式 detach 是接口为短命 logger 提供的正规用法。
头文件同时承诺首次读取旧属性时发出弃用警告,后续访问不重复警告。同步兼容测试因此仍有独立职责:它证明 attach 区间内 getter 返回 URL 并保留一次性警告;异步 timeout 回归则证明 detach 以后同一 getter 可以安全完成而不进入 logger。
部分回移会留下不同失败形态。只移动 filler,迟到 getter 仍可能进入失效 logger;只加 logger guard,读取该成员前已经通过失效 filler;只加 DCHECK,release 二进制没有任何运行时改变;只 detach 某个错误出口,其他出口仍可把旧地址带入 checkpoint。
SetLogger() 把原本藏在构造函数里的长期借用改成 null、attached、再次 null 的状态机。控制流审计应以 attach 为起点:attach 之前的返回可以绕过 detach,之后每条通往作用域退出的路径都必须先经过 null setter;debug DCHECK 则禁止 attached 状态到达 filler destructor。
固定源码还存在一条更早的零时限出口。若调用方明确给出非正 reporting timeout,函数在建立 isolate、filler 和 recycler 之前就回传 reportWin() aborted due to zero timeout.,标记 script_timed_out 为真,elapsed 为零。它没有创建 lazy getter,也不会经过本 CVE 的 Promise-teardown 时序,现场日志应把它与执行中超时分开。
接下来两组参数失败发生在 scope 与 filler 已经建立、logger 尚未 attach 的区间。auction signals、per-buyer signals 或 seller signals 无法转换为 V8 参数时,函数回传空输出并把 timeout 标为 false;direct-from-seller signals 无法写入对象时也从这里退出,并保留已经收集的转换错误。因为 v8_logger_ 仍是 null,这些 return 不需要先调用 detach。这个前置区间正好说明显式状态优于“所有 return 都机械清理”:补丁让每条退出路径根据是否跨过 SetLogger(&v8_logger) 决定动作,此前只需依赖 filler 活过 scope,之后必须先置 null。
顶层脚本成功以后,代码才向 recycler 增加 report、beacon、macro、Private Aggregation 与可选 Shared Storage bindings,然后调用 reportWin 或 reportAdditionalBidWin。这项分期解释了输出差异:顶层失败时相关 binding 尚未开放,函数调用失败时脚本已经可能留下 PA 请求,清理路径因而选择保留它们。
顶层 RunScript() 与后续 CallFunction() 共用同一个名为 total_timeout 的 TimeLimit 对象,可选的 reportAggregateWin 调用也继续传递它。测试中的无限循环消耗的是整段 bidder reporting 预算,没有在进入 reportWin() 时获得一只新的计时器;事故对照要保存同一 reporting-timeout 配置。
构造 browserSignals 时,固定源码先把显式 browser_signal_reporting_timeout 或默认 AuctionV8Helper::kScriptTimeout 归一成 reporting_timeout,再交给 SetBrowserSignals();创建总时限时则把原始 optional 交给 helper。测试记录应同时保存调用方是否显式设置和最终使用值,避免把默认限额与业务传入值混成一次无法复核的“超时”。若遥测只保留一个 timeout 布尔值,零时限早退与执行中耗尽预算就会落入同一桶;精确 errors、elapsed 和阶段枚举可以把它们重新分开,两者的响应语义并不相同。
函数开头的 DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_) 还给时序定性补上一项源码证据:这段实现要求在同一受检 sequence 调用。漏洞重现无需假设两个工作线程同时争用地址,Promise、终止、栈退出与手工 checkpoint 已能在这条序列上完成全部动作。
上报室里的变化因此十分具体:黄铜抽屉和开锁助手留到关门程序结束,通向日志台的线只在工作人员值班时接通。Promise 票据晚到仍可取回 URL 卡片,却没有机会叫回已经离场的 logger。
5.2 recycler 先收起连接再放行微任务,卖方路径同时清掉误导性借用
提交的第二层修改发生在 ContextRecycler::ResetForReuse()。新版保留同一批 reset 操作及其内部排列,只把整个区块移到 time-limit scope 与 PerformMicrotaskCheckpoint() 前面;任何延迟 JavaScript 恢复前,recycler 自己拥有的上一轮 native 连接都已经进入清洁状态。这项重排保证本轮 context 在清理回调前撤销 native 连接,却不是栈上 deprecated filler 的直接生命周期修正;后者不在 reset 列表中,靠提前构造与延后析构存活。
同一提交还检查 seller worklet。匿名辅助函数 AppendAuctionConfig() 删除一个实际上没有使用的短生命周期 AuctionV8Logger* 参数,递归处理 component auction 的调用也同步删除该参数;SellerWorklet::V8State::ScoreAd() 与 ReportResult() 随后去掉只为传给该辅助函数而创建的局部 logger。AuctionConfigLazyFiller 一直从 ContextRecycler 取得 recycler-owned logger,补丁只是让源码表达与真实拥有关系一致。
提交说明称,若 config filler 错用传入的短命 logger 会带来麻烦。实际 AuctionConfigLazyFiller 由 EnsureAuctionConfigLazyFillers() 使用 recycler-owned logger 构造,删除的参数从未被它采用。公开 diff 因而只证明预防性接口清理,不能被升级成另一项已证实漏洞。
六个改动文件分成三组:bidder_worklet.cc 与 deprecated filler 的头、实现修正直接生命周期;bidder 单测锁定 Promise-timeout 因果链;context_recycler.cc 与 seller_worklet.cc 完成通用清理。冲突解决若丢掉任一组,回移记录都要明确差距。
主线提交由 Maks Orlovich 完成,2026 年 6 月 26 日合入 main@{#1653488},评审编号 8006698,Change-Id 为 I570a1267be21d9d398c4e2102e21a6bb923345c2;M150 的对应回移是 4e15f05d550a3c0f7268a11e85427b0c6330e525,于 7 月 1 日落到 branch-heads/7871。这些标识让不同分支的 cherry-pick 能够追溯到同一逻辑修改,Stable 标签与平台构建的对应关系则需另结合 Included-In 和产品发布记录判断。
固定 diff 记录此次变更为六文件共 65 行新增、29 行删除。这组规模能帮助来源核对,却不是回移完整性的替代品;不同分支可能因上下文变化产生不同统计。功能性清单仍应逐项确认。
提交说明中的 “other lazy fillers seem fine on read-through” 是维护者当时的审阅结果,并非其他对象永远安全的形式化证明。reset 重排仍有明确价值:在异步 cleanup 之前统一撤销宿主连接,减少新 filler 依赖人工记住同一顺序的机会。
声明顺序的补丁在 merge conflict 中最容易被无意改坏。下游若把新 filler 构造放回参数准备附近,即使内容完全相同,逆序析构保证也会消失;代码评审应检查最终文件位置,不仅确认新增行存在。logger detach 的覆盖则可从 attach 节点验证:每条通往作用域退出的路径都必须经过 null setter,只有 attach 之前的返回路径可以绕开。
reset 重排的验收则看支配关系:所有 recycler-owned reset 必须支配 checkpoint。若下游新增一种 filler,却把 reset 放在 checkpoint 后,新对象仍可能重演相似风险,即使本 CVE 的 deprecated helper 已经安全。
这里还有两只容易混淆的 logger。ContextRecycler::GetContext() 在首次创建 context 时生成一只 recycler-owned AuctionV8Logger,许多 binding 与 auction-config filler 借用它;ReportWin() 又为当前调用建立一只栈上 v8_logger,deprecated URL filler 的 SetLogger() 接通的是后者。补丁在 seller 辅助接口删除短命参数,正是为了让两种来源不再看起来可以互换。
AddBindings() 先要求 context 已存在,再把 binding 挂入 context,最后把裸指针登记进 bindings_list_;具体 binding 仍由 recycler 的 unique_ptr 成员拥有。reset 遍历的是这份登记表,因此“先 reset、后 checkpoint”覆盖已经暴露给脚本的全部 binding,不会因为某个成员藏在不同字段里而漏掉。
auction-config filler 的容器还有一条特有契约:同一个 worklet 可能在不同 auction 中看到不同数量需求,源码只扩容、从不缩小,因为外部可能仍保存指向较后元素的地址。补丁没有靠删除这些 filler 解决问题,而是在 drain 前逐个 Reset();这与 deprecated URL 栈对象的延寿形成两种清晰的处理方式。
6 回归要证明超时按预期返回,版本要按平台证明修复真的送达
6.1 这段 fixture 的五类输出为空,timeout 与唯一错误仍必须完整返回
上游异步回归最有价值的地方,是把内存安全修复与业务收尾放在同一个断言里。旧构建可能在 scope teardown 中进入失效 filler;修复构建必须安然返回,同时仍把这次调用视为 timeout,不能为了避免 getter 而误报成功。RunReportWinExpectingResult() 对 report URL 使用 std::nullopt,对 beacon map、macro map 与 PA request vector 使用空集合,对 PMT request pointer 使用 null;optional、map、vector 与 pointer 由 helper 按强类型逐项比较,能发现序列化成“无结果”后看不出的绑定层差异。
测试同时要求 expected_reporting_latency_timeout 为 true,并对错误向量做全等比较。若修复吞掉 timeout、增加额外脚本错误,或让本来没有登记输出的 fixture 返回意外结果,用例都会失败。安全通过不能牺牲宿主 API 的可观察语义。
测试 helper 还会确认 script_timed_out 为真,并检查 reporting latency 至少达到显式 reporting timeout 或默认脚本限额的九成。比例断言证明执行确实到达时限终止,同时容纳线程偏差;下游改变限额时应记录新配置,不要改成绝对毫秒等待。
异步用例没有连接 inspector,也不检查 console event。相邻的 ReportWinBrowserSignalRenderUrlDeprecationWarning 才创建 ScopedInspectorSupport,把 debugger session 接到测试 worklet,并运行 sendReportTo(browserSignals.renderUrl);它先要求 report URL 正确返回且无 timeout、无业务错误,再等待一条 warning。这里并非宽松匹配关键词:固定测试核对完整文本 browserSignals.renderUrl is deprecated. Please use browserSignals.renderURL instead.,还要求 stack trace 只有一层、函数名为 reportWin、来源等于 bidder URL、行号为 10,最后调用 ExpectNoMoreConsoleEvents(),重复告警或归属漂移都会被看见。
推荐字段由另一项 ReportWinBrowserSignalRenderUrlNoDeprecationWarning 单独覆盖。它同样连接 inspector,执行 sendReportTo(browserSignals.renderURL),确认 URL 正确、timeout 为 false、errors 为空,然后要求没有任何 console event。大小写不同的两个字段因此拥有清楚分开的兼容基线。
两项同步测试旁都留有移除旧字段后的清理 TODO,说明 deprecated getter 是有计划退出的兼容层;在它真正删除以前,安全回移仍须保留现有取值与首次警告语义。直接把 renderUrl 改成普通值,虽然可能让异步 UAF 用例不再触发,却会绕开上游选择的生命周期修复。
负面对照可分别排 Promise 但不 timeout、timeout 但不读取旧 getter、异步读取 renderURL,并连续执行两轮 invocation。它们帮助区分目标时序与一般 timeout、字段选择或 reset 漂移,不把单次差异过度解释成因果证明。
回移验收不能只跑新增测试。至少还要覆盖脚本编译失败、顶层运行失败、reportWin() 抛异常、正常上报和超时上报;每条在 logger attach 之后离开的路径都应证明已 detach。新增 early return 时,debug 析构断言尤其有用。
错误向量“全等且唯一”还有聚类价值。若下游运行同时出现脚本语法错误、signals 转换错误或网络加载错误,测试可能没有到达目标 getter 时序。先让 fixture 只产生 timeout,再判断内存行为,能减少把早期失败误当修复通过。
三种构建回答不同问题:ASAN 捕捉失效栈对象访问,debug 构建用 DCHECK 检查 detach,接近生产的优化构建验证错误、timeout 和兼容输出没有配置漂移。结果应分别记录;上游 TEST_F 本身没有显式 ASAN 栈断言,不能把任一种通过替代另外两种目的。
实验室可以把同一份 bidder body 拆成四个镜头。第一个同步读取 renderUrl,确认兼容 getter 和一次性警告仍工作;第二个把读取交给 Promise 但让函数正常返回,观察常规 checkpoint;第三个在超时脚本里读取推荐的 renderURL,排除普通字段被误改;第四个才是上游的旧 getter 加 timeout 组合。四次运行使用同一源码修订与限额,分别锁定兼容性、调度政策、字段类型和目标 teardown 时序。
6.2 Stable 同一天交付 .114 与 .115,不能压成一条通用小于号
Google 2026 年 7 月 8 日桌面 Stable 公告列出的平台构建并不相同:Windows 与 macOS 获得 150.0.7871.114 或 150.0.7871.115,Linux 获得 150.0.7871.114,并在随后数日或数周滚动发布。VersionHistory API 分别记录了 Windows .114、Windows .115、macOS .114、macOS .115 与 Linux .114 的 serving release;Linux .115 查询没有对应发布记录,因此运营清单应按平台接受公告中的修复构建。
CVE CNA 与 NVD 为了表示单一 Chrome 产品范围,统一使用 prior to 或 versionEndExcluding 150.0.7871.115。这是数据库归一化口径,无法表达同一轮 Stable 的双构建;若机械套用,会把已含修复的 Linux .114 判成脆弱版本。M150 回移提供了源码侧交叉证据:提交 4e15f05d550a3c0f7268a11e85427b0c6330e525 的 Included-In 页面列出 .114 与 .115 标签,说明这两个 Stable 号码都可以包含同一修复,部署规则应优先采用平台公告与 vendor provenance。
Windows 或 macOS 的 .114 也不能因为小于 .115 就自动判定未修复。相同四段版本号在不同平台和 rollout 阶段有发布分歧,资产系统至少要同时保存 platform、channel、完整版本和运行进程时间。对 Chromium 衍生浏览器、CEF、Electron 类 runtime 或定制 embedder,Chrome 版本号更没有直接约束力;供应商可以使用不同 branch、回移哈希与功能配置,最强证据是厂商公告、固定源码修订或可复核 diff,再连接到实际二进制摘要。
同 Change-Id 还回移到 M149、一个 7827_199 分支与 M144,其中 M149 Included-In 可指向 149.0.7827.238。这些记录只证明代码进入相应源码线,不能自动推出 LTS、ChromeOS 或第三方产品已经发布,每个受支持产品仍需自己的发布说明。升级完成也不等于运行时已经安全:浏览器农场、kiosk、长时间桌面会话和测试 worker 可能持有旧进程,运行中 pod 也可能来自旧 digest;轮换证明要对应发现路径,分别检查包与签名、PID 与启动时间和摘要、image digest 与新副本创建时间。
资产优先级可结合三项:是否启用或测试 Protected Audience,是否接受非自有 bidder script,以及相关进程能否长期不重启。普通终端、广告实验室和自动化集群的风险形态不同,修复版本要求却都必须落到实际运行引擎。rollout 还意味着同一组织在 7 月 8 日之后可能短期同时看到 .114、.115 和旧版本;状态面板需要逐资产记录,不能把公告发布日期当成全网完成日期,升级 SLA 应从设备实际收到并重启修复包开始测量。
VersionHistory 的 serving 记录证明某个平台版本正式分发,不证明组织中的每台设备已经取得它。反过来,企业可能通过离线包先部署相同签名构建;这时本地包 provenance 与进程摘要可以补足 rollout API 的时间差。
版本策略落地时,可把规则写成平台集合:Windows/macOS 接受两项修复构建或更高厂商确认版本,Linux 接受 .114 或更高厂商确认版本,downstream 接受固定 revision 集合。集合表达比单一阈值更忠实,也更容易审计例外。
软件清单还要处理多安装并存。系统级 Chrome 已更新,用户目录里的 portable Chromium、Playwright 下载缓存或旧测试镜像仍可能被任务启动;按路径和摘要枚举实际二进制,才能发现这些绕过包管理器的副本。
修复测试与发布版本之间最好建立 digest 关系。若测试用带 ASAN 的内部 artifact,至少证明它和 release 包来自同一 source revision,再用生产配置跑兼容冒烟;否则“回归通过”和“用户收到修复”仍是两条断开的证据。
版本回退监控也应识别 .115 切回含修复 .114 的合法平台变化,不能看到数字下降就报警。真正的失败条件是离开已确认修复集合,平台信息始终是规则的一部分。
资产报告还应保存 channel。Stable、Extended Stable、Beta 和自建分支即使显示相近主版本,也可能拥有不同发布时间与 backport;上述桌面矩阵只直接证明对应公告覆盖的发布渠道。
发布证明还要保留采集日期。VersionHistory 与 Included-In 页面会随着新标签和 rollout 状态更新;在 7 月 18 日复核时可见的映射,应与抓取时间一起归档。未来页面新增版本时,历史案件仍能解释当时为何接受某个构建。
7 调查要把源码、产品发布与事件进程落到同一条资产时间线
7.1 最后一帧只说明倒在哪里,完整时序才说明为何倒下
第一只调查时钟是源码:主线 5a68bc6c... 于 6 月 26 日合入、M150 4e15f05d... 于 7 月 1 日回移,下游也有各自纳入等价改动的时间,它回答“修复逻辑何时进入这条源码线”。第二只时钟是产品发布:Google 在 7 月 8 日公告桌面 Stable 的平台构建,rollout 仍可能延续数日或数周,它回答“供应商何时向某个平台和渠道提供修复包”,不等同于某台设备已经安装。
第三只时钟是运行进程。包管理器可以显示 .114,旧 browser process 却仍从更新前存活;自动化节点也可能从缓存目录启动另一份 Chromium。它回答事件发生时究竟执行哪一个二进制,往往决定案件能否归入受影响集合。崩溃记录因此要能唯一定位该二进制:完整版本、平台、channel、process type、启动时间、可执行路径、artifact digest 与 build ID 缺一不可;auction worklet 可能由专用服务进程承载,外层窗口标题和安装器版本都不够。
符号化后可寻找 DeprecatedUrlLazyFiller::HandleDeprecatedUrl、AuctionV8Logger::LogConsoleWarning、ContextRecycler::ResetForReuse、scope destructor、V8 checkpoint 与 BidderWorklet::V8State::ReportWin。优化可能内联或裁掉其中一些帧;关键不是每个名字整齐出现,而是复原 getter、logger、reset 与 checkpoint 在崩溃前后的先后关系。
符号文件必须匹配同一 build ID。拿邻近 .114 或 .115 的符号套用旧 dump,可能把偏移和内联函数映射到错误名称;符号服务器查询结果、符号摘要和崩溃 artifact 应作为一组保存。
应用层再补上 bidder owner、脚本来源、auction 关联标识、reporting timeout、console deprecation event 与页面修订摘要。资源内容只保留必要 digest,调查可以证明“同一脚本语义”而无需长期复制竞价画像或完整用户数据。
三套日志还要校时:浏览器事件可能使用单调时间,服务遥测使用 UTC,终端更新记录使用本地时区。案件表应保存原始值、时区、转换规则与允许误差,防止把更新后的安全进程配给更新前的 crash。
证据可先分为暴露、相符异常和确认事件。脆弱进程本身只证明暴露;timeout 加进程重启仍可能是业务故障。提高归因前要确认二进制资格、目标 worklet 进程、同一 invocation 的 reporting timeout,以及 lazy getter 与 teardown 相邻的符号或 sanitizer 证据;再用修复版对照记录反证,防止只收集支持最初假设的日志。
没有 minidump 时,crash-loop 分组、服务异常退出、worklet host 重启和同一内容在修复构建上的结果仍有价值。传感器也可能漏记微任务或在 dump 上传前失去进程,所以报告应写“未发现证据”并标注缺口;它既不能证明从未触发,也不能证明已经实现代码执行。
修复前后对照应固定 bidder script digest、页面修订、timeout、feature flags、进程模型与插桩配置。若脚本在两次运行间更新,或仅一侧开启 ASAN,观察差异就不再只由 Chromium revision 解释;若确认修复的 artifact 仍出现同一 native 链,应回查符号、回移完整性和其他生命周期缺陷,不能凭版本标签关闭案件。
最后一帧只说明程序倒在哪里,完整时序才解释为何倒下。调查应沿同一调用重建信封保存、Promise 排队、timeout、栈回退、scope checkpoint 与 native getter;这一故事顺序同时也是最短的证据索引。
现场记录可以用一个 invocation ID 串起六个节点:auction 建立、bidder script 装载、reportWin() 开始、timeout 判定、worklet service 异常和 dump 写入。每个节点保留单调时间与进程标识;跨主机汇总时再附 UTC。若中间某一节点缺失,案件图应显示断点,不能用相邻事件自动补成完整链。
7.2 没有可靠流量签名,版本证明与受控对照更接近真相
这项 UAF 发生在浏览器进程内部的 JavaScript/C++ 交接处,网络请求只负责交付网页与 bidder script。相同语义可以由无数种源码写法实现,TLS 还会遮蔽内容,因此搜索上游测试函数名、无限循环字面量或 renderUrl 字符串都不是可靠 IDS 规则。reportWin timeout 本身也很常见,脚本错误、网络数据异常、性能回退和测试故障都能制造 timeout,旧字段弃用警告也可能来自正常兼容代码;两项遥测只有在脆弱构建、微任务清理和 native 异常同时出现时才提高调查价值。
全网排查先从版本与源码修订清单开始。Chrome 按平台矩阵检查,downstream runtime 记录 vendor evidence,容器同时检查 image digest 与 live process,portable 或自动化缓存扫描真实可执行文件;发现脆弱实例后优先升级和轮换。验证自有回移时,则在无生产数据的隔离环境运行上游回归和负面对照,逐项确认 timeout 收尾、同步弃用兼容与 context 清理,并把测试输出绑定到待发布 artifact。
补丁发布与证据保全可以并行。先保存现有 minidump、日志、二进制摘要和页面修订,再更新进程;无需把脆弱浏览器继续暴露在网络上等待第二次 crash,修复后的安静运行本身也是闭环证据。对 embedder 还要确认 Protected Audience 相关 feature 是否编入、是否启用,以及 auction worklet 由哪个 process 承载;功能关闭可以改变可达性,却不能把含漏洞源码说成已修复,开关变化还会重新改变暴露。
网络日志回答脚本从哪来,应用遥测回答 auction 何时运行,crash 记录回答原生进程怎样退出,版本与摘要回答它是否仍含漏洞。四类证据指向同一次调用时,才足以把这次异常认定为安全事件。
在补丁尚未完成分发的短窗口,受管理环境可以依据产品能力暂时停用相关实验或限制不可信 bidder 来源,以降低可达性。此类措施必须有厂商支持和回滚计划,并明确标注为临时控制;它们不会改变二进制仍含缺陷的事实。
自有 bidder 迁移到 renderURL 能减少旧 getter 和警告噪声,也让异常基线更干净。第三方内容仍可能访问旧别名,因此脚本治理与引擎升级仍是两条并行任务。
崩溃簇可按 build ID、process type、顶层 native 模块和时间窗分层,再在簇内寻找 getter 与 checkpoint 线索。先按稳定字段分组,能避免符号缺失时把所有 V8 crash 混进一个无法调查的大桶。
若 sanitizer 明确报告 stack-use-after-scope,仍要核对被标记变量是否为 deprecated_render_url 或 logger 相关对象。相同错误类别可能来自其他 worklet 代码;变量、源码位置和回归时序共同决定归因。
8 从固定提交走到运行进程,最后让那张 Promise 票据安全取件
8.1 修复账本要串起源码、构建、包、进程、测试与回滚
账本从固定源码起步。主线提交、Gerrit 评审、M150 回移、六个改动文件和回归测试都使用固定 URL 或完整哈希;同一页再标出 filler 声明顺序、logger 状态机、getter guard、析构 DCHECK、reset-before-checkpoint 与 seller 参数清理。这样,“包含上游修复”才被拆成能逐项回到 downstream diff 的行为证明。
源码离开仓库成为二进制后,证据链继续记录 source revision、编译器与 flags、feature configuration、symbols 和 artifact digest。供应商若再打包,签名 package 必须能反查原始构建;Chrome 还要保存 platform 与 channel,容器保存 image digest,embedder 保存外层版本到 Chromium revision 的映射。测试一份内部产物、发布另一份包,链条就在这里断开。
包落盘只是中点。轮换后读取 browser 与 worklet-host 的 PID、启动时间、可执行路径、完整版本和摘要,清掉孤儿 worker、portable 副本与自动化缓存。安装成功日志回答文件是否到达,live process 回答事件时究竟执行了哪一份指令,两者缺一都不能把资产标为已修复。更新器可能已经替换磁盘文件,旧进程却仍映射先前 image;容器 tag 也可能已经指向新 digest,旧副本仍在运行。事件判断要保存当时的进程或实例摘要,不能事后用同一路径的新文件反推。
回归结果也要落到同一 artifact:异步 timeout 用例、同步旧字段警告、推荐字段无警告、阶段失败和多轮 invocation 隔离分别保存 ASAN、debug 与接近生产配置的输出。观察窗再连接 process revision、reportWin timeout 基线、相关 crash group 与 rollback 事件;timeout 不必归零,旧修订进程和相符 native fault 才是需要清零的对象。
等价回移可以改函数名,却不能改掉四项性质:callback owner 覆盖最后 checkpoint,短命 logger 在离场前断开,宿主连接先 reset,因果回归仍以同样 timeout 语义返回。行为证明既不会错拒安全重构,也不会因几行文本相似就放过位置错误的声明。
8.2 关门程序先拔线、收连接,再把取件票交给仍在场的助手
实际响应可以按下面次序执行,每一步都对应前文的一只时钟或一件原生对象:
- 按平台部署 Chrome 修复构建。Windows 与 macOS 接受 Google 公告中的
150.0.7871.114或.115,Linux 接受.114;下游产品取得自己的厂商或 revision 证明。签收记录同时写入 channel、包摘要与证据来源。 - 轮换所有常驻进程。重启桌面会话、浏览器农场、kiosk、广告测试 worker、容器副本与嵌入服务,并读取 live process version、启动时间和 executable 摘要,直至旧 PID 清零。
- 清点上报路径。找出启用 Protected Audience、执行第三方 bidder script 或频繁运行 auction worklet 的产品与测试系统;对 portable binary、浏览器下载缓存和旧测试镜像单独枚举。
- 验证完整回移。核对 filler 声明顺序、logger attach/detach、getter guard、recycler reset 次序、seller 清理和六文件来源;从 attach 节点走完每条退出路径,保留最终 diff。
- 运行因果回归。执行异步 Promise-timeout 用例、同步弃用兼容测试、推荐字段测试、负面对照和多轮 invocation 隔离,保存原始日志、构建摘要、限额配置与强类型结果对象。
- 保全并复查崩溃。关联脆弱进程、reportWin timeout、V8 checkpoint、lazy getter 与 logger 相关 native 帧,区分暴露、相符异常和已证实事件;归档匹配 build ID 的符号来源。
- 迁移自有旧字段。把 owned bidder code 改用
renderURL,减少弃用噪声,并用同步兼容测试观察第三方脚本;这项兼容治理不能替代浏览器安全更新。 - 锁住安全回滚点。禁止调度器、缓存和应急镜像重新启动未含修复的 revision,为安全 artifact 建立不可变白名单,并在观察窗后完成双向证据签收。
现在回到开头的上报桌。红铃响起后,Promise 票据依旧可能晚到;补丁没有假装队列会消失。它让黄铜抽屉的 filler 留到 scope 完成,让 logger 在工作人员离场前明确拔线,也让 recycler-owned 连接在托盘转动前先收起。
于是 PerformMicrotaskCheckpoint() 最终恢复 getter 时,callback data 找到的仍是有效 filler。URL 可以照常返回,logger 已经是 null,不再进入上一段脚本的日志对象;checkpoint 完成以后,filler 才按逆序析构。
四项改动各自回答一个验收问题:声明顺序保护 callback owner,attach/detach 约束短命借用,getter guard 定义 cleanup 行为,reset 顺序则让其余 recycler-owned state 在 checkpoint 前先完成清理。上游因果回归直接锁定 filler 存活、logger detach/guard 与 timeout 结果;recycler-owned reset 顺序和 seller 预防性清理没有由这一 TEST_F 单独断言,需要用固定 diff 与专项控制另行签收。
公开时间线也在此合拢:6 月 24 日研究者报告,6 月 26 日主线修复,7 月 1 日 M150 回移,7 月 8 日 Stable 按平台交付。四个日期分别属于发现、源码、分支与产品时钟,任何一个都不能独自证明某台机器已经安全。
若 V8 日后移动 terminating checkpoint,要重新确认这项 fixture 仍在 teardown 中恢复 deprecated getter,必要时加入 trace、断点或定向断言。若 reaction 提前落到正常 checkpoint,即使 timeout 输出仍相同,一次通过也不再证明原来的清理时序已经受测。
签收后保留一份最小复核包即可:Stable 公告、固定 diff、构建摘要、运行版本、原始测试结果与观察窗结论。它足以支持下一次审计,也能避免把完整 bidder script、用户数据或竞价画像长期复制进安全档案。
最终,桌上的票据只是一项被正确延后的工作。蓝色信封仍归 V8 管理,旧抽屉仍服务兼容脚本;被消除的是票据与过期栈地址之间的错误连接。故事从一次超时开始,到运行进程的证据签收结束,始终沿着同一张取件票前进。
研究记录
9证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
9.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
Chromium InterestGroups reportWin 释放后使用
reportWin deprecated URL handler 生命周期问题
main@{#1653488},评审 8006698
branch-heads/7871 回移
Promise、旧 getter 与 timeout 回归
终止后延迟 checkpoint 重入 getter
9.2事件时间
- 研究者报告问题
Jihyeon Jeong 向 Google 报告 InterestGroups 释放后使用。
- Chromium 主线修复合入
提交 5a68bc6c 以 main@{#1653488} 落地。
- M150 回移合入
提交 4e15f05d 进入 branch-heads/7871。
- Chrome Stable 公告发布
Google 交付桌面平台修复构建并公开 CVE-2026-15133。
- SOSEC 完成源码重建
SOSEC 固定六文件 diff、回归断言、平台版本与运行时验证链。
9.3来源与材料
- Chrome 2026 年 7 月 8 日桌面 Stable 安全更新https://chromereleases.googleblog.com/2026/07/stable-channel-update-for-desktop_01162222768.html
- CVE-2026-15133 记录https://www.cve.org/CVERecord?id=CVE-2026-15133
- CVE Program 官方 JSONhttps://cveawg.mitre.org/api/cve/CVE-2026-15133
- NVD CVE-2026-15133 页面https://nvd.nist.gov/vuln/detail/CVE-2026-15133
- Chromium 主线修复 5a68bc6chttps://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13
- Chromium 评审 8006698https://chromium-review.googlesource.com/c/chromium/src/+/8006698
- 修复后的 bidder_worklet.cchttps://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/bidder_worklet.cc
- 异步 reportWin 回归测试https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/bidder_worklet_unittest.cc#8715
- ContextRecycler 重置顺序https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/context_recycler.cc#185
- LazyFiller context 生命周期契约https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/lazy_filler.h
- DeprecatedUrlLazyFiller 接口契约https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/deprecated_url_lazy_filler.h
- DeprecatedUrlLazyFiller 实现https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/deprecated_url_lazy_filler.cc
- seller worklet 防御性清理https://chromium.googlesource.com/chromium/src/+/5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13/content/services/auction_worklet/seller_worklet.cc
- M150 回移 4e15f05dhttps://chromium.googlesource.com/chromium/src/+/4e15f05d550a3c0f7268a11e85427b0c6330e525
- M150 回移 Included-In 标签https://chromium-review.googlesource.com/changes/8031201/in
- Windows Stable 150.0.7871.114 发布记录https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions/150.0.7871.114/releases
- Windows Stable 150.0.7871.115 发布记录https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions/150.0.7871.115/releases
- macOS Stable 150.0.7871.114 发布记录https://versionhistory.googleapis.com/v1/chrome/platforms/mac/channels/stable/versions/150.0.7871.114/releases
- macOS Stable 150.0.7871.115 发布记录https://versionhistory.googleapis.com/v1/chrome/platforms/mac/channels/stable/versions/150.0.7871.115/releases
- Linux Stable 150.0.7871.114 发布记录https://versionhistory.googleapis.com/v1/chrome/platforms/linux/channels/stable/versions/150.0.7871.114/releases
- Protected Audience 竞价上报指南https://privacysandbox.google.com/private-advertising/protected-audience-api/ad-auction-reporting
- Protected Audience / FLEDGE 公开说明https://github.com/WICG/turtledove/blob/main/FLEDGE.md