漏洞
V8 Turboshaft 初始化写入被错误删除(CVE-2026-15132)
CVE-2026-15132 让 V8 Turboshaft 在循环展开和存储消除组合下删除 FixedArray 必需的初始化写入,垃圾回收器因而读取未初始化槽位并破坏渲染进程内存安全。

文章导航
垃圾回收器停在一个刚刚发布的三格数组前,前两格都是它熟悉的 the_hole,第三格却仍像分配器刚交出的毛坯房;真正写入 42 的指令明明就在后面,却隔着一次主垃圾回收,再也赶不上这次检查。CVE-2026-15132 的危险就藏在这段短得近乎无事发生的时间里:Turboshaft 把首次建立对象合法状态的写入误当成可以由后续覆盖替代的普通存储,于是优化器删掉了垃圾回收器此刻唯一需要的那一笔。
1 垃圾回收器赶到时,第三格还没有身份
1.1 分配到内存,不等于已经造好对象
V8 中的 JavaScript 数组通常由多个堆对象配合表示。JSArray 保存 map、属性、elements 指针和长度,真正的带标签元素则放在独立的 FixedArray 中。内存分配只取得一块空间,并不会自动让里面每个机器字都成为有效的 V8 标签值。对象一旦进入垃圾回收器能够追踪的位置,所有带标签槽位都必须可以安全解释;新建数组通常先用 hole 哨兵值填满这些位置。
Turboshaft 用类型状态表达这段构造期。Allocate<AnyFixedArray>() 返回 Uninitialized 包装,头部字段通过 InitializeField() 写入,元素也应使用带初始化语义的存储。FinishInitialization() 是明确的交接点:调用后,对象可以像普通堆对象一样流入后续图节点。存储优化会读取初始化与对象迁移元数据,以判断分配附近的写入能否合并、跨越或删除,因此这套状态并非只服务于代码可读性。
旧实现同时破坏了类型状态和操作元数据。它写完 map 与长度后立刻调用 FinishInitialization(),随后才执行元素循环;循环使用 StoreNonArrayBufferElement(),最终生成的存储没有初始化标记。编译图看到的是“已经完成构造的对象被普通写入”,实际情况却是这些写入首次让 backing store 的槽位具备可回收性。
V8 的堆不会把“尚未写完”当作可任意暴露的普通状态。原始区域可以短暂未初始化,但对象地址一旦进入可达图,标记器就会按 map 描述逐槽解释机器字,不会等待剩余存储。hole 因此不是装饰性填充,而是一个 GC 能理解的真实值:它把“JavaScript 语义上尚无元素”与“该机器字尚无法解释”分开。前者可以长期存在,后者只能留在受控构造窗口内。
Uninitialized<T> 和 StoreOp 元数据共同记录这扇窗口。前者提醒 C++ 调用者对象尚未完成,后者告诉优化器某次写入正在建立合法状态;两者都不是自动屏障。旧代码先用 FinishInitialization() 关闭类型状态,又让后续首次写入沿默认参数退化成普通覆盖。源码看上去仍依次完成分配、头部写入和 hole 填充,只有把完成点与最终 StoreOp 属性放在一起,才会发现对象在最后一次首次写入之前已经被宣布完成。
公开证据同时确定了缺陷与边界。V8 提交说明,缺失的 annotation 与循环展开等优化结合后,会让初始化存储被消除,使 GC 观察未初始化内存;补丁 diff 将标记传到下层并移动完成点,回归样例则把确定性的主回收放在早写与晚写之间。公开材料没有给出通用沙箱逃逸、稳定堆塑形方法或在野利用记录;架构、分配器状态、指针压缩和原始比特会影响后续结果,但垃圾回收器读到一个从未成为合法标签值的机器字,本身已经构成渲染器内存安全缺陷。
1.2 一条初始化语义在五层辅助函数中走丢
MachineLoweringReducer::ReduceNewArray 先按头部大小和元素数量计算 backing store,写入 map 与长度,再循环填入 hole。受影响版本却在建立 ScopedVar<WordPtr> index 和 WHILE 之前,就把 Uninitialized<AnyFixedArray> 交给 FinishInitialization();循环拿到的基址已经是普通 array。最了解数组归属和构造用途的 reducer,就在这里丢掉了下游无法自行推断的信息。
随后,StoreNonArrayBufferElement() 把访问描述、基址、索引和值交给私有 StoreElement(),后者计算对齐、表示、写屏障、头部偏移与缩放,再调用通用 Store()。旧接口没有表达“这次写入正在构造对象”,用于标识初始化或 map 迁移的 maybe_initializing_or_transitioning 因而保持默认 false。最终图节点只看见地址、表示与普通覆盖写入,看不见它正在建立 GC 不变量。
普通同址写入可以被更晚的写入支配,前提是中间没有观察者需要旧值。这里的观察者是 GC:JavaScript 没有读取第三格,回收器却会按 map 解释它。初始化存储不仅生产程序值,还把内存从不可解释状态变成合法对象字段;晚于 GC 的赋值无法倒过来完成这次转换。
根因因此不是“store elimination 太激进”,写屏障也不能代替缺失的属性。消除阶段依照收到的语义工作,写屏障维护引用关系,而初始化标记描述对象生命周期。断裂发生在新对象借用共享元素写入路径却没有携带构造意图时;修复恢复信息,让原有优化规则仍可删除真正冗余的普通覆盖。
下游分支即使函数名或模板不同,也可以沿同一条路径复核:新 backing store 在哪里分配,未完成类型何时变成普通对象,哪个 helper 产生元素写入,哪个最终操作决定初始化语义。把这四点接上,下一章的三次循环、偏移 16 与 GC 才从一组偶然数字变成一处可重复的语义断点。
2 三次循环展开,把一扇窄窗推到灯下
2.1 偏移 16 的早写与晚写之间站着一次主回收
上游回归主动塑造了缺失标记会产生后果的编译图。两棵异或表达式以不同顺序表示同一旋转;TurboFan 没有先确认相等,数组长度仍是 3 与 4 之间的 phi,Turboshaft 随后识别旋转并把长度折叠为 3。常量恰在这一阶段出现,使不超过三次的填充循环被完全展开。
展开后的 hole 填充成为固定偏移存储。样例把数组发布到 glob.x,调用 %MajorGCForCompilerTesting(),然后执行 arr[2] = 42。在目标指针压缩布局中,第三个 FixedArray 元素位于偏移 16;JSArray 的四个头字段写入则落在 0、4、8、12,因而不会与这对元素存储混淆。
缺少初始化属性时,优化器把偏移 16 的 hole 写入与稍后的 42 视作两次普通同址写入。早写被删除后,已经逃逸到全局根的数组在主回收时留下未初始化标签;晚写发生在观察之后,无法修复回收器已经读到的状态。这正是“存储消除”如何转化为“未初始化使用”。
三和十六都是回归配置的定位条件,不是漏洞常量。三同时命中当前展开门槛和布局隔离;十六来自该指针压缩对象布局。其他架构或未来版本可能改变阈值和偏移,验证应重算地址并保留同一关系:早写与晚写指向同一元素,其他头部写入不与之混淆,GC 位于两者之间。
glob.x 也不可省略。它让 backing store 从 reducer 的局部区域逃逸,使 GC 能从根集合抵达对象;否则标量替换或分配折叠可能把测试带到另一条路径。强制主回收则把真实运行中概率性的观察窗口固定成确定顺序,让缺陷能够反复核查。
最终机器码少一条 hole 写入,看起来可能只是正常优化;GC 的最终故障又可能远离编译器。只有把中间图、对象发布与回收点排在同一时间轴上,才能看出早写承担了“让对象此刻可扫描”的职责。下一节继续解释回归为何用一组看似古怪的输入,确保这条时间轴真的形成。
2.2 0x80000000 留住了决定循环形状的分叉
样例先用 0x80000000 预热 foo(),再输入普通小整数。这个不能编码成 Smi 的 Int32 迫使反馈同时容纳 Smi 与 HeapNumber,并保留一条 phi;否则更早的表示化简可能改写左移表达式,使旋转匹配器无法在 Turboshaft 阶段证明两棵异或树相等。输入值在这里不是业务数据,而是塑造中间表示的工具。
rest 参数、两次预热和 %OptimizeFunctionOnNextCall(foo) 也属于同一构图计划。若原生语法被禁用、优化关闭或函数去优化,脚本仍可能正常返回,却没有进入目标 tier。验收必须证明长度在预期阶段折叠为三、循环完全展开,并记录 V8 构建和启动参数。
完整因果链分成两半:旋转识别、常量三与展开把元素填充变成可比较的固定存储;glob.x、主回收和稍后的 42 则让错误删除成为可观察的未初始化读取。缺少前半段,图形不会形成;缺少后半段,GC 看见的仍可能是合法数组。
耐久回归因此需要两层结果。外层确认优化调用正常完成;内层确认数组在 GC 前逃逸,第三格仍有初始化 StoreOp,且该操作没有被晚写消除。循环阈值、布局、表示选择或存储消除以后变化时,绿色结果只有在这些前置条件仍成立时才有意义。
三、十六、0x80000000 和四十二分别固定展开门槛、元素地址、混合反馈与同址覆盖。它们是定位针,不是利用常量;跨架构或回移时可以重算或替换,但必须保留“初始化写成为删除候选—对象可达—GC 观察—晚写随后到达”的顺序。
测试专用内建和强制回收也不是网络特征。普通页面不需要逐字复制这些语句,等价 JavaScript 又可以被压缩或动态生成;寻找样例字符串只会命中测试,无法证明生产引擎安全。可执行控制仍是修订核验、回归验证和更新。
这组条件最终只服务于一个问题:缺失属性是否真的让优化器把对象合法化写入当成普通覆盖。补丁不追逐四枚定位针,而是恢复贯穿所有长度、布局和地址的初始化身份。
3 补丁没有加护栏,它把丢失的身份送到底层
代码与处置。缺口位于 V8 固定提交 a90e80ff481a0689ba1a835f3dbcbf9a078076a5 所修改的 src/compiler/turboshaft/assembler.h 与 machine-lowering-reducer-inl.h:MachineLoweringReducer::ReduceNewArray 过早调用 FinishInitialization(),元素 helper 又没有把 maybe_initializing_or_transitioning 送到最终 StoreOp。永久修复必须同时延后完成点并传播初始化属性;桌面 Chrome 应按平台进入 2026 年 7 月 8 日的固定线——Windows/macOS 为 150.0.7871.115,Linux 为同一公告中的 150.0.7871.114——下游与嵌入式产品则需供应商确认等价回移并彻底结束旧进程。无法立即升级时,只能暂时收紧不可信脚本入口、缩短会话或隔离产品;关闭 JavaScript、匹配回归字符串或等待崩溃都不能代替修复。
3.1 初始化标记沿每一层 store helper 继续向下
修复让 StoreNonArrayBufferElement()、类型化 StoreElement() 和私有元素写入实现逐层接收 maybe_initializing_or_transitioning,再把它交给底层 Store()。普通写入继续使用默认 false;InitializeElement() 与 InitializeNonArrayBufferElement() 明确传 true,使构造意图穿过 API 封装。
同一补丁让 reducer 在元素循环中继续持有 uninitialized_array,每次调用专用初始化 helper,直到循环退出才执行 FinishInitialization()。接口传播与完成点移动缺一不可:前者校正优化器看到的属性,后者校正类型世界中的生命周期。
提交同时修改汇编器接口、machine-lowering-reducer-inl.h 和 regress-527385397.js。三者分别保存语义、使用时机和失败图形,应被视作一个原子意图。下游模板若不同,可以用强类型枚举或专用 helper 表达同一性质,但不应把裸 true 散落到调用点,也不能只看到新参数便停止在上层。
补丁没有更换 hole、数组尺寸、写屏障或优化开关。普通覆盖仍可消除,循环仍可展开,数组仍会逃逸;变化只是首次写入不再冒充普通覆盖。这种窄改动让回归结果能够直接归因于初始化语义恢复,而不是某条性能路径被整体关闭。
3.2 完成关卡回到最后一个元素写入之后
新 reducer 让未完成包装贯穿整个填充过程,最后一个带标签槽位写好之后才允许 backing store 成为普通对象。零长度数组经过空循环即可完成;一至三格的展开写和更长数组的保留循环都使用同一初始化语义,所以修复覆盖的是生命周期,而非第三格特例。
未完成类型也让未来危险改动更显眼:循环期间若新增可能发布对象或触发 GC 的操作,普通堆对象 API 与当前类型之间会产生可见摩擦。类型不能独自证明安全,却把原先藏在注释里的构造窗口提升为接口状态。
回移应从两个后置条件出发:对象可追踪之前,每个 GC 槽位都是合法标签;建立这一性质的存储不能跨越观察点被消除。类型状态以 FinishInitialization() 为刻度,优化器语义以 initializing/transitioning 属性为刻度;两个时钟必须在最后一次首次写入之后同时越线。
这份双重证据分别落在 assembler.h 与 machine-lowering-reducer-inl.h。前者让元素包装接受并转交属性,后者让未完成数组留在循环中。只出现一边变化,说明回移仍然只有半个修复。
父提交 ff4b5e8651c5b9af8d563a10c515642cd6638dc8 固定受影响基线,评审 8005985 连接补丁集与最终提交,主线位置 #108296 提供线性顺序。产品包含关系仍应优先由最终哈希祖先关系或等价回移证明;位置编号、DEPS 和供应商说明只能按各自强度补充。
regress-527385397.js 与代码同批落地,保存了混合反馈、逃逸、GC 与晚写必须共存的原因。分支可以改名或重构样例,但不能为了变绿删掉这些因果角色;测试最终要证明的是早写至少存活到所有观察者之后,而不是禁止后续 42 正常覆盖 hole。
把意图放进专用初始化 API,使未来元素表示、helper 重构或优化顺序变化仍有可搜索的检查点。修复覆盖的是“首次建立合法状态”这一性质,不会因为目标从第三格换成第五格而失效。
4 同一段 V8,会穿着许多不同产品的外衣
4.1 Chrome 稳定版给出第一条可执行版本线
Chrome 在 7 月 8 日桌面稳定版中修复这项 V8 未初始化使用。Windows 与 macOS 的固定构建是 150.0.7871.115,Linux 在同一发布线使用 150.0.7871.114;资产策略必须保存平台和完整四段版本,不能把 Windows 末段套到 Linux,也不能只比较主版本 150。
下游浏览器、发行版和嵌入式运行时使用自己的版本与交付时钟。CVE 范围适合筛选候选,真正的修复证明则是 V8 修订 a90e80ff481a0689ba1a835f3dbcbf9a078076a5、等价回移或明确的供应商构建映射。一个不同的产品版本可能已经回移,显示“Chromium 150”的包也可能仍早于修复。
公开材料确认 JavaScript 优化与 GC 能形成渲染器内存安全条件,却没有完整利用链或在野利用记录。优化 JavaScript 属于正常网页能力,风险优先级应依据代码可达性、内存安全后果和内容暴露,而不是公开 PoC 是否存在;分配器、架构和沙箱配置影响后续可利用性,不改变更新必要性。
V8 主线提交时间与 Chrome 稳定版发布时间也不相同。评审从 6 月 26 日开始,提交在 6 月 29 日进入 V8 主线位置 #108296,稳定版公告在 7 月 8 日公开。处于这段时间窗的自编译快照、每日构建和固定主线修订必须按提交包含关系判断;“构建日期晚于修复讨论”不够,真正的 ancestor 关系或源码清单才够。
自动更新又引入安装与运行两种状态。分阶段推送、离线设备和长期会话会让固定包已经落盘、旧 renderer 仍继续处理内容;清单应区分未上线、未下载、待重启与正在运行安全构建,并以进程版本和最近重启时间关单。
Extended Stable、企业通道、WebView、移动端和专用衍生产品都需要各自供应商证据,不能从桌面公告外推具体版本。尚未映射的产品留在未知状态,直到厂商给出修复线或引擎修订。
“是否包含受影响代码”和“何时优先更新”是两个问题。代码血缘决定适用性;内容来源、用户规模、沙箱和业务角色决定顺序。下一步因此不是继续解释 Chrome 版本,而是找到所有没有 Chrome 名字、却实际加载这段 V8 的进程。
4.2 外层版本号不能替代内层引擎修订
第一圈是 Chrome Stable、Extended Stable、Beta、Chromium 发行包和下游浏览器。每行记录平台、架构、通道、安装与运行版本、更新来源和最近重启;同为 150.x 的不同通道不能折成一个状态。
第二圈是 Electron、CEF、Qt WebEngine、WebView 和定制桌面壳,第三圈是直接嵌入 libv8 的服务、测试工具、插件宿主和设备界面。系统 Chrome 更新不会替换这些私有引擎,“没有浏览器 UI”也不能排除影响;软件物料清单应继续向下映射 Chromium/V8 修订和实际加载模块。
自编译或等价回移需要两层证明:源码中 uninitialized_array 穿过整个元素循环,maybe_initializing_or_transitioning=true 抵达最终 StoreOp;交付层则给出包含该性质的产品构建、发布时间和重启要求。只有二进制时,可组合符号、源码清单、可复现构建元数据和供应商声明。
当二进制来源无法确定时,风险状态应保持“待证明”,不能自动归入安全或受影响。可以先用版本与构建时间筛选,再由应用负责人向供应商索取引擎修订;在证明返回前,减少自动打开不可信内容、缩短更新周期并监控异常渲染器退出。这样的临时控制有明确终点:拿到引擎证据或替换构建,不会无限期停留在“关注”。
同一应用可能在安装器、更新器、主界面和插件宿主中携带多份引擎;容器又可能同时包含系统 Chromium、应用下载和 CI 缓存。资产记录要绑定进程实际加载的模块路径,容器则绑定镜像 digest、缓存键和创建时间,防止旧目录或回滚重新唤醒受影响库。
开发工作站上的 Stable、Canary、Playwright/Puppeteer 下载与 Electron 依赖同样属于范围。除了清理当前缓存,还要更新锁文件和下载配置,避免下一次干净安装重新取得旧构建。
每项证据保留引擎修订、产品构建、工件摘要、来源链接和采集时间。供应商页面、about 值和缓存都会变化;只有带时间的结构化记录,才能重建当时运行状态并在范围变化后重新计算暴露。
5 崩溃现场散得很远,线索仍能重新汇合
5.1 从 GC 阶段、优化历史和构建号重新聚类
调查通常从一条普通 renderer 终止开始。先固定完整浏览器构建、V8 修订、系统与架构、指针压缩、启动参数、页面或应用来源、minidump 和 V8 代码事件,再看堆栈;缺少构建就无法把事件放到补丁前后,缺少来源也难以连接多台设备上的重复故障。
最终故障可能出现在标记、压缩、对象遍历或更晚的元素访问,而不会留下 MachineLoweringReducer。MemorySanitizer、AddressSanitizer 和 debug 断言又会分别截获未初始化值、派生访问和对象校验。聚类应结合引擎构建、已优化函数历史和 GC 阶段,不能只按最后地址或报错类别切开。
高价值现场把页面或应用版本、脚本摘要、崩溃时间、浏览器工件、符号源、优化/去优化日志和 dump 绑在一起,并遵守既有的数据最小化要求。只保存 URL 会丢失快速变化的 bundle,只保存 dump 又可能无法还原脚本和 tier。
确认构建受影响后,可以在隔离环境用同版本、同架构与公开回归核对符号、GC 路径和修复前后差异。保存完高价值工件即可更新生产设备;隔离副本足以继续分析,任何新出现的影响判断都应保持为假设,直到证据能够支持它。
普通 JavaScript 可以来自网络、本地页面或应用生成内容,因此不存在可靠的流量签名。引擎来源与运行版本负责预防,崩溃检测负责连接现场和发现交付漏点;两者的职责不能互换。
5.2 安静遥测只说明没有捕获到证据
许多环境不上传 renderer dump,自动恢复又可能只留下退出码。“三十天没有命中”首先说明传感器覆盖,而不是漏洞未触发。报告计数时应同时说明多少进程上传 dump、哪些架构有符号、是否保留优化事件,以及嵌入式产品是否接入同一管线。
运营指标应围绕可关闭事实:受影响引擎数量、固定包下载、进程重启、未知嵌入修订和旧构建上的崩溃。它们能暴露“包已安装、进程未换”和“外层已升、内层未知”,比尝试统计每一种 JavaScript 触发更可靠。
异常聚类按构建、架构、来源和 GC 邻近性分层,并把函数优化、数组发布、GC 首次解释第三格、最终故障四个时刻分开。相同受影响构建在相似脚本和回收阶段重复失败会提高优先级,却仍不能单独证明利用;缺少一致性也不妨碍更新。
版本证据覆盖整批资产,崩溃证据只提高个别事件的调查优先级。安静设备仍共享错误语义,完成运行态升级后也仍可分析已保存现场。遥测是回声,不是门锁:无需等待本地指标,修复应先抵达所有运行二进制。
6 回归通过之前,先证明它真的走过危险图形
6.1 JavaScript 正常结束只是最外层结果
先用 V8 测试框架和所需原生语法,在每个目标架构运行 test/mjsunit/turboshaft/regress-527385397.js。正常结束只是外层结果;日志或图转储还要证明 foo 进入目标 Turboshaft 路径、旋转关系在预期阶段化简、三次循环展开,避免解释执行或去优化制造假绿。
来源记录连接上游修复、父提交、评审、主线位置和下游回移;构建则使用产品真实 GN 参数、指针压缩、架构和工具链,并保存编译器、V8 revision、测试命令与工件摘要。测试内建只在测试框架启用,不能变成生产启动配置。
图中三条 hole 写入必须携带初始化/迁移属性,GC 前的第三格写入仍应存在;普通数组覆盖则继续获得合法的 store elimination。下游 helper 栈即使不同,也必须让每个带标签元素在对象可追踪前成为合法值,并让建立该性质的写入一路保持初始化语义。
交付证据把通过测试的源码、最终签名包、部署批次和重启后的运行进程连成同一修订链。若测试 A、发布 B,或磁盘已经是 B、进程仍运行 A,源码验收并没有抵达产品。
旧平台无法运行上游 harness 时,可在同源测试构建中执行回归,再用产物来源连接生产库;未覆盖架构必须写明负责人和补测时间。源码、图、mjsunit、工件摘要与进程采样应相互独立,避免一条“测试通过”日志同时为所有结论自证。
6.2 长度、架构与 GC 配置构成验收矩阵
长度覆盖 0、1、2、3、4 和一个明显更长的值,分别检查空循环、短展开、原始三格窗口、策略变化和保留循环。每项都在对象发布或 GC 观察前确认所有标签槽合法。
架构覆盖实际交付的 x64、arm64 和其他目标。偏移 16 不是跨平台断言;每个平台都按标签宽度、指针压缩和对象布局重算地址,确认目标元素不与头部写入混淆,早写在 GC 前、晚写在后。
主回收是上游已证覆盖;年轻代回收、增量标记和压力压缩属于下游扩展,不能自动扩大公开影响。循环展开、store elimination 和表示选择的逐项开关用于定位因果,最终构建仍须在正常优化配置下安全通过,不能靠关闭 Turboshaft 交付。
关闭证据包保留修订包含关系、两处源码性质、上游回归与扩展矩阵、最终工件摘要、运行进程版本和明确的未覆盖项。优化阈值以后变化时,这些记录仍能解释为什么当时相信第三格在 GC 到达前已经合法。
长期分支可增加两道轻量守卫:禁止未完成对象流入普通元素写入 API,并在图测试中断言 GC 前的初始化 StoreOp 未被删除。它们不能取代端到端回归,却能在 helper 改名、模板重构或优化顺序变化时直接指出语义在哪一层断开。
7 修复要抵达正在运行的最后一个渲染器
7.1 先封住版本窗口,再保存高价值现场
- 确定最低安全构建。Windows 与 macOS 采用 Chrome 150.0.7871.115,Linux 采用稳定版公告列出的对应修复构建,下游产品必须取得厂商等价修复证明。
- 列全嵌入式引擎。覆盖桌面客户端、启动器、协作工具、IDE、kiosk 外壳、测试运行器和打包 Chromium 或 V8 的设备。
- 部署并重新启动。更新后采集正在运行的进程版本,长期会话与自动拉起服务需要专门安排退出和重启。
- 验收源码回移。同时检查推迟的
FinishInitialization()、带初始化语义的元素存储和跨架构上游回归。 - 复核异常渲染器崩溃。保留受影响构建中的高价值现场,优先处理发生在 GC 或优化数组附近的故障。
- 保留修订证据。把供应商包、Chromium 位置、V8 提交和发布结果写入资产记录。
先冻结低于平台安全线的新装与回滚,再用 Chrome 公告和 V8 修订把产品版本连接到源码修复。Windows/macOS 可直接核对 150.0.7871.115,Linux 发行包则依据各自安全公告、变更记录或 V8/Chromium 映射,不能把浏览器外观版本当作唯一证明。
范围按“哪些进程能够执行不受信任或半受信任 JavaScript”展开,而不只看默认浏览器。Electron、CEF、Qt WebEngine、自动化浏览器、kiosk 和直接嵌入 V8 的进程都需要自己的安全版本、供应商证据和重启路径;内容来源和隔离能力只决定部署次序。
每个批次同时保存安装前后包版本、进程路径、启动时间和运行版本。后台更新、存活标签页和托盘应用都可能让旧引擎继续工作;必要时整机重启,直到旧 renderer 离场,补丁才真正抵达执行 GC 的代码。
无法立即升级时,可以缩短会话、限制高风险内容入口并提高证据保留,但这些措施必须有负责人、到期日和替换计划。发现可疑崩溃时先保存 dump、构建、脚本摘要与时间线,然后照常更新;隔离副本支持后续分析,生产设备无需为取证继续暴露。
供应商需给出首个安全产品版本、发布时间、所含 Chromium/V8 修订、直接合入或等价回移方式,以及是否必须完全退出。内部维护分支则必须同时移植汇编器属性、reducer 完成点和回归,记录因 API 差异作出的等价调整;“使用最新版”或只摘测试都不能关单。
外部内容多、用户多、长期不重启或隔离较弱的产品可以先部署,但离线笔记本、实验室镜像、会议室设备和自动化节点仍要进入后续批次。回滚目标也必须高于安全底线;兼容性问题只能转向后续固定构建、在修复分支处理或暂时下线功能,不能重新放出受影响引擎。
7.2 重启、构建证明和回归结果必须落在同一资产记录
资产记录连接源码修订、构建产物、部署批次和运行进程。官方 Chrome 可以使用稳定版公告、完整版本和受管设备回报;自建 Chromium 还需 V8 revision、DEPS、构建参数、摘要与签名;闭源嵌入产品则明确标出哪些环节只能依赖供应商声明。
回归结果必须绑定最终工件。日志保存源码提交、构建 ID、架构、关键 GN 配置、命令和结果,签名或重新打包后再次提取引擎标识。上游 mjsunit 证明公开图形,产品场景证明真实链接和配置;冒烟测试不能替代编译器回归。
运行态分两次核验:部署后确认新进程比例,一个业务会话后再次寻找旧 renderer、GPU 或 utility 进程。只要仍有旧 renderer 接收内容,磁盘版本再新也不能关闭风险。
看板使用已确认受影响、固定包可用、已部署待重启、运行态已验证、修订未知和已下线等状态,并预先定义更新失败、长期离线、拒绝重启、兼容性阻断和供应商不响应的处理分支。未知不能自动变绿。
最终记录应能重建产品为何受影响、哪一修订改变两处生命周期语义、哪个包携带它、何时部署、哪些进程退出、哪些架构通过回归以及哪些假设仍未覆盖。这样关闭的不只是工单,也是一条以后回移和发布可以复用的证据链。
8 灯再次扫过第三格,它已经不再空白
8.1 已证实事实与仍然未知的利用条件
公开记录已经把根因连成一条完整链:V8 提交说明,缺失的初始化 annotation 与循环展开等优化结合后,会让初始化存储被消除,随后由垃圾回收器读到未初始化内存;diff 将这一属性沿元素存储辅助函数传到下层,并把 FinishInitialization() 移到填充之后;回归样例则把长度化简、三次展开、全局发布、主回收和稍后覆盖放进同一条执行路径。这些材料相互印证,无需借助未经证实的利用故事。
Chrome 稳定版公告把这条源码链落到产品上,将 CVE-2026-15132 定为 V8 中的高危未初始化使用并给出修复版本,因此受影响引擎需要尽快退出运行。公开材料没有给出完整的沙箱逃逸、持久化或稳定利用方法,也没有报告在野利用;这只能说明现有公开证据没有建立那些结论,不能反过来证明攻击从未发生。
上游回归同样不能用来量化利用可靠性。它借助测试专用内建,在恰当位置强制主回收,以最短路径证明 GC 能观察错误优化后的对象;普通网页还要通过正常分配与回收压力抵达等价时序。原始机器字如何被解释,又取决于堆状态、标签表示、指针压缩、架构和构建配置。这些变量增加了稳定利用所需的工程,却不削弱已经证实的内存安全风险。
处置结论因此很直接:浏览器及所有嵌入 Chromium/V8 的产品都应进入供应商确认的修复版本,并彻底重启相关进程。版本、引擎修订和运行态证据比来源不明的 PoC 更安全、更可重复,也能覆盖尚未崩溃的资产;若设备已经出现异常,则在升级前后保留现场材料,以便后续证据出现时重新判断。
8.2 构造状态必须活到最后一次首次写入
回到故事开头,垃圾回收器其实从未做错事。它沿着一个已经发布的 JSArray 找到 backing store,读取 map 所承诺的三个带标签槽位,并按 V8 的对象模型解释它们。真正的谎言发生得更早:编译图已经宣告对象构造完成,却还欠第三格一次使其成为合法标签的写入。GC 只是第一个把这份欠账翻出来的观察者。
优化器也不是因为“太激进”而随机删错。若同一地址稍后必然被覆盖且中间无人观察,删除早写本来就是正确优化;问题在于编译图没有告诉它 GC 也是观察者,更没有告诉它这次早写首次让槽位成为合法值。补丁没有关闭循环展开或存储消除,而是让 Uninitialized 状态继续穿过元素填充,直到最后一格写好才结束构造,使原有优化重新获得真实前提。
这也给代码与测试留下了清晰的不变量:对象何时第一次能被 GC 找到,那一刻哪些槽位必须合法,以及每次首次写入的元数据是否抵达最终内存操作。三次循环、反馈差异、全局发布和主回收共同把这个不变量变成可重复的图级观察;专用初始化 API、参数与回归则避免下一次辅助函数重构再次依赖“大家都知道这是初始化”。同样的审查还应覆盖完整观察者集合,因为调试器、去优化、异常路径和并发运行时组件也可能依赖对象布局始终有效。
这条语义链在部署侧有一面镜子:修复要依次穿过 V8 提交、Chromium 集成、产品打包、设备安装和进程重启,才能抵达终端上真正执行 GC 的渲染器。源码中的标记若没到 StoreOp,第三格仍会失去身份;修复修订若没到运行进程,设备仍在执行旧逻辑。只有最后一次首次写入、产品修订和运行进程落在同一条时间线上,灯再次扫过第三格时,看到的才是从分配到发布始终合法的对象。
研究记录
9证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
9.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
Chrome 150 稳定版修复的 V8 Turboshaft 未初始化使用
元素存储初始化标记逐层传递
backing store 分配与 hole 填充
三次循环展开、存储消除与 GC 覆盖
9.2事件时间
- V8 修复开始评审
初始化标记与定向回归样例进入代码评审。
- 修复进入 V8 主线
提交 a90e80ff481a 落在 V8 主线位置 108296。
- Chrome 发布稳定版安全公告
Chrome 将问题编号为 CVE-2026-15132,并纳入 150.0.7871.115 安全发布线。
- SOSEC 完成源码复核
从本地源码对象复核辅助函数传递、编译图条件与全部回归输入。
9.3来源与材料
- Chrome 2026 年 7 月 8 日稳定版安全更新https://chromereleases.googleblog.com/2026/07/stable-channel-update-for-desktop_01162222768.html
- CVE-2026-15132 记录https://www.cve.org/CVERecord?id=CVE-2026-15132
- V8 修复 a90e80ff481a 与变更文件https://chromium.googlesource.com/v8/v8/+/a90e80ff481a0689ba1a835f3dbcbf9a078076a5
- V8 代码评审 8005985https://chromium-review.googlesource.com/c/v8/v8/+/8005985
- 上游 Turboshaft 回归测试https://chromium.googlesource.com/v8/v8/+/a90e80ff481a0689ba1a835f3dbcbf9a078076a5/test/mjsunit/turboshaft/regress-527385397.js
- 修复提交中的 Turboshaft 汇编器接口https://chromium.googlesource.com/v8/v8/+/a90e80ff481a0689ba1a835f3dbcbf9a078076a5/src/compiler/turboshaft/assembler.h
- 修复提交中的机器降低 reducerhttps://chromium.googlesource.com/v8/v8/+/a90e80ff481a0689ba1a835f3dbcbf9a078076a5/src/compiler/turboshaft/machine-lowering-reducer-inl.h
- 修复前父提交 ff4b5e8651c5https://chromium.googlesource.com/v8/v8/+/ff4b5e8651c5b9af8d563a10c515642cd6638dc8
- V8 官方 Turboshaft 架构说明https://v8.dev/blog/leaving-the-sea-of-nodes
- V8 官方垃圾回收调度说明https://v8.dev/blog/free-garbage-collection
- V8 官方嵌入指南https://v8.dev/docs/embed
- V8 官方源码检出指南https://v8.dev/docs/source-code
- V8 官方测试运行指南https://v8.dev/docs/test