Longinus:一个漏洞中的两个边界,利用单一漏洞穿透Chrome的渲染器和V8沙箱,CVE-2026-6307

Lobsters Hottest 新闻

摘要

Chrome V8 JIT编译器中的单一漏洞CVE-2026-6307,允许攻击者在V8沙箱内获得任意读写原语并逃逸沙箱,实现远程代码执行,影响自Chrome 106及更高版本。

<p><a href="https://lobste.rs/s/uaoe9y/longinus_2_boundaries_one_bug_piercing">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/29 16:30

# Longinus:一个漏洞中的两个边界,用单一漏洞刺穿 Chrome 的渲染器和 V8 沙箱,CVE-2026-6307 来源:https://nebusec.ai/research/v8-cve-2026-6307-writeup/ 1. https://nebusec.ai/ 2. 研究 (https://nebusec.ai/research) 3. Longinus:一个漏洞中的两个边界,用单一漏洞刺穿 Chrome 的渲染器和 V8 沙箱,CVE\-2026\-6307 > Chrome V8 JavaScript 引擎具有堆沙箱,旨在防止攻击者仅利用 JavaScript 引擎中的漏洞就能在沙箱区域外进行写入。然而,Vega (https://nebusec.ai/vega/) 在 JIT 编译器中发现了的一个特殊漏洞,允许攻击者在沙箱内获得任意读写原语,甚至仅凭该漏洞就能逃逸沙箱并在其外部进行写入。本文档将涵盖该漏洞的技术细节。 ## 摘要 该 V8 漏洞本身就能实现以下所有功能: - 以 100% 的成功率获得任意内存读写原语,无需任何喷射技巧 - 仅凭该漏洞即可实现 V8 堆沙箱逃逸和 RCE,无需其他漏洞 - 在 Chrome 106 中发现,跨越 4 年 您的浏览器不支持视频标签。 ## 背景 要理解这个漏洞,首先需要从高层次了解 TurboFan、它是如何内联 JS 到 Wasm 调用的,以及它的去优化元数据如何决定在惰性去优化后应重建什么类型的值。此外,我们需要了解 V8 堆沙箱的工作原理,以及为什么这个单一漏洞允许攻击者同时做两件事:1) 在沙箱内获得任意读写原语,2) 逃逸沙箱并在其外部进行写入。 ### TurboFan TurboFan 是 V8 的优化编译器。当某个 JavaScript 函数运行足够多次后,V8 可以利用较低执行层收集的反馈来编译该函数的专用版本。这些反馈包括对象的形状、调用点处看到的目标、以及特定操作所使用值的表示形式等信息。V8 最初通过 Ignition 字节码执行函数,在 TurboFan 编译它之前可能还会经过其他层。这些早期执行会填充该函数的反馈向量。TurboFan 将字节码和反馈转换为编译器图,应用高级 JavaScript 优化,最终将图降低为机器操作。Turboshaft 是此流水线中使用的低级编译器表示形式,与这个漏洞相关的值编号行为也发生在这里。 从高层次看,TurboFan 的任务是用专门的机器代码替换通用的 JavaScript 执行,同时保留一条回到正确通用执行的路径。它可以内联被调用函数、专业化属性访问、并发出检查来守护从反馈中做出的假设。如果其中某个假设后来失效了,优化后的执行必须去优化。 #### 节点之海 TurboFan 将要编译的函数表示为一个图,通常称为“节点之海”。与简单的线性指令列表不同,操作通过它们的依赖关系连接起来。一个节点可以依赖值输入、控制输入和效果输入。这使得 TurboFan 能够移动和简化操作,同时仍然保留副作用和控制流所要求的顺序。在传统的 CFG 中,编译器会问:这条指令属于哪个基本块,它以什么顺序执行?但在节点之海中,编译器问的是:这个操作依赖什么,以及后来可以合法地将其放在哪里?这种区别很重要,因为节点之海避免过早地确定指令的确切位置。 例如: `` function f(x, y, z) { let a = x + y; let b = y + z; if (a > 0) return b * 2; else return b * 3; } `` 在 CFG 中,你可能将其表示为: `` B1: a = x + y b = y + z if a > 0 goto B2 else B3 B2: return b * 2 B3: return b * 3 `` 这里,`b = y + z` 已经被放置在块 B1 中。而在节点之海中,`b = y + z` 只是一个依赖于 `y` 和 `z` 的 Add 节点:它不必立即属于某个基本块。稍后,编译器可以决定是将其放在分支之前、放入某个分支内部、提升它、下沉它、消除它、还是与另一个相同的计算共享它。这就是关键好处:更多的优化自由度。 节点之海特别擅长:公共子表达式消除、全局值编号、死代码消除、代码移动、边界检查消除。最后,它将节点之海图调度到基本块中,将节点降低为机器指令,分配寄存器,移除 Phi 等,然后生成线性汇编/机器代码。 对于这个漏洞,重要的点是:调用、检查和去优化元数据都存在于同一个图中。当 TurboFan 内联一个调用时,被调用函数的操作会被插入到调用者的图中。如果被内联的操作可以触发去优化,图中还会包含 `FrameState` 节点,描述如果优化执行无法继续时应如何重建执行状态。`FrameState` 节点是元数据,但它们仍然是带输入和选项的图节点。它们不像算术或内存操作那样执行,但优化通道仍然可以对其进行推理。稍后在文档中,这一点很重要,因为两个 JS 到 Wasm 延续的 `FrameState` 节点在图优化器看来可能等价,即使它们描述了不同的 Wasm 返回类型。 #### JS 到 Wasm 调用内联 与本漏洞相关的一个 TurboFan 优化是 JS 到 Wasm 调用内联。JavaScript 和 WebAssembly 使用不同的调用约定和值表示,因此从 JavaScript 到 Wasm 的调用通常通过一个 JS 到 Wasm 封装器进行。封装器将 JavaScript 参数转换为 Wasm 值,执行 Wasm 调用,然后将 Wasm 结果转换回 JavaScript 值。TurboFan 可以将该封装器内联到优化的 JavaScript 调用者中。这避免了单独的封装器调用,并将参数/结果转换代码暴露给优化器。V8 也可能内联足够小的 Wasm 函数体,但完整的 Wasm 体内联对于此漏洞并非必需。内联封装器就足够了。 在本文档中,JS 到 Wasm 调用是通过 JavaScript 属性 getter 到达的。经过预热后,TurboFan 可以专业化属性访问,检查接收器形状,直接调用已知的 getter 目标,并内联该目标的 JS 到 Wasm 封装器。如果同一个 JavaScript 函数遇到两种接收器形状,优化后的图中可以包含两条这样的 getter 路径,位于同一个编译函数内。 每个内联的封装器是根据其 Wasm 函数的规范签名构建的。规范化允许 V8 用共享的签名对象表示结构上等价的函数类型。签名包含参数和返回类型;因此它比仅仅记录延续内置函数接收到多少个值要更精确。对于这个漏洞,两种返回类型很重要。Wasm `i64` 在 JavaScript 中暴露为 `BigInt`,而 `externref` 是一个带标签的 JavaScript 引用。 对于具有此签名的 Wasm 函数: `` (func (result i64)) `` 机器返回值是一个原始的 64 位整数。封装器必须将该整数转换为 `BigInt` 才能返回给 JavaScript。 对于这个函数: `` (func (result externref)) `` 返回寄存器中的值已经是一个带标签的引用,必须按此处理。机器级别的返回位置可能相同,但这些位的含义由 Wasm 签名决定。重要的细节是,封装器签名决定了 Wasm 返回位如何转换回 JavaScript。 ### 去优化 去优化用可以继续在较低层级执行的一个或多个帧替换活动的优化帧。为了实现这一点,V8 必须恢复未优化函数所期望的状态:它的参数、局部变量、上下文、当前字节码位置以及任何内联的帧。优化后的代码不一定以原始形式保留这些值。局部变量可能存储在寄存器中、折叠为常量或完全移除。因此,编译器会在执行可能离开优化代码的点上附加去优化元数据。在 TurboFan 和 Turboshaft 中,这种状态用 `FrameState` 节点表示。 简化的 `FrameState` 包含: - 重建帧所需的值,例如参数和局部变量。 - 当当前操作被内联到另一个函数中时的外层 `FrameState`。 - 描述帧类型、延续点以及函数特定元数据的 `FrameStateInfo`。 编译器可以嵌套这些状态。如果 getter 及其 JS 到 Wasm 封装器已被内联到优化调用者中,内层延续状态指向外层 JavaScript 状态。去优化器会遍历这个链来重新创建逻辑调用栈,即使这些调用在优化的机器代码中不再以独立的物理帧存在。 有两种相关的去优化方式。**立即去优化**当检查失败时立即发生。此时,重建帧所需的所有值在去优化点处都是可用的。**惰性去优化**与调用相关联。当另一个函数正在执行时,优化函数可以被标记为需要去优化,但转换只会在该调用返回时发生。惰性去优化需要对调用结果进行特殊处理。结果在 `FrameState` 创建时并不存在,因此不作为普通输入列出。相反,去优化器从机器返回寄存器中获取结果,并将其添加到一个延续帧中。然后执行通过一个延续内置函数恢复,就好像优化调用正常返回了一样。 对于由 TurboFan 内联的 JS 到 Wasm 调用,V8 会创建一个内层延续 `FrameState`,其类型为 `kJSToWasmBuiltinContinuation`。它的函数信息使用一个派生类,该派生类存储了 Wasm 签名: `` class JSToWasmFrameStateFunctionInfo : public FrameStateFunctionInfo { public: const wasm::CanonicalSig* signature() const { return signature_; } private: const wasm::CanonicalSig* const signature_; }; `` 这个签名不仅仅是信息性的。在代码生成过程中,V8 从中派生 Wasm 返回类型,并将该类型序列化到去优化数据中。如果发生惰性去优化,去优化器会使用记录的返回类型来物化结果。 #### 物化 Wasm 返回值 对于 JS 到 Wasm 惰性去优化,在机器代码层面上调用已经返回。因此,去优化器从返回寄存器和记录的 Wasm 返回类型重建调用结果: `` TranslatedValue Deoptimizer::TranslatedValueForWasmReturnKind( std::optional<wasm::WasmReturnKind> wasm_call_return_kind) { if (wasm_call_return_kind) { switch (wasm_call_return_kind.value()) { case wasm::kI32: return TranslatedValue::NewInt32( &translated_state_, static_cast<int32_t>(input_->GetRegister(kReturnRegister0.code()))); case wasm::kI64: return TranslatedValue::NewInt64ToBigInt( &translated_state_, static_cast<int64_t>(input_->GetRegister(kReturnRegister0.code()))); case wasm::kF32: return TranslatedValue::NewFloat( &translated_state_, input_->GetFloatRegister(wasm::kFpReturnRegisters[0].code())); case wasm::kF64: return TranslatedValue::NewDouble( &translated_state_, input_->GetDoubleRegister(wasm::kFpReturnRegisters[0].code())); case wasm::kRefNull: case wasm::kRef: return TranslatedValue::NewTagged( &translated_state_, Tagged(input_->GetRegister(kReturnRegister0.code()))); default: UNREACHABLE(); } } return TranslatedValue::NewTagged(&translated_state_, ReadOnlyRoots(isolate()).undefined_value()); } `` 去优化器不会询问原始的 Wasm 函数它返回了什么;它信任从 `FrameState` 序列化的 `wasm_call_return_kind`。对于 `kI64` 和引用返回,它读取相同的机器返回寄存器 `kReturnRegister0`。唯一的区别在于如何解释这些位:`kI64` 将寄存器值强制转换为 `int64_t` 并将其包装为 `BigInt`,而 `kRef` 和 `kRefNull` 将寄存器值包装为 `Tagged`。这也是为什么当与 `i64` 混淆时,`externref` 情况会泄漏完整的 64 位带标签值。指针压缩影响堆字段中存储的许多带标签值,但编译器图中的 Wasm 引用值使用的是带标签的寄存器表示形式。在指针压缩构建中,该寄存器表示形式是一个解压缩的堆指针或一个 Smi。当去优化器读取 `kReturnRegister0` 时,不存在单独的“32 位压缩指针加笼基地址”对需要重建;寄存器中已经包含完整的带标签值。因此,如果记录的返回类型来自错误的 `FrameState`,去优化器会直接将相同的 64 位重新解释为错误的 JavaScript 值类型。这就是 `externref` 可以被物化为 `i64`,或者 `i64` 可以被物化为对象引用的关键点。 #### FrameState 合并 一个微妙的点是,`FrameState` 节点在被序列化到去优化数据之前仍然是编译器图中的节点。这意味着图优化也可以看到它们。这里相关的优化是公共子表达式消除,也称为全局值编号。公共子表达式消除本身不是去优化器的一部分,但它会影响去优化器随后消费的元数据。如果两个图节点具有相同的输入和等价的元数据,编译器可以保留第一个节点并将第二个节点的所有使用替换为它。对于普通操作,这消除了冗余工作。对于 `FrameState` 节点,这意味着两个去优化状态可以被合并。 仅当两个状态对于去优化是可互换时,合并两个 `FrameState` 节点才是安全的。Turboshaft 首先使用快速哈希来定位可能的匹配,然后使用相等运算符进行完整比较。该比较必须包含每个影响帧重建方式的字段。`FrameState` 哈希故意只使用元数据的一小部分,包括它的 bailout ID。哈希冲突是预期的,本身不是漏洞:在找到具有相同哈希的候选后,值编号会比较操作的输入和完整选项。因此,正确性边界就是相等比较。如果它说语义上不同的去优化状态是相等的,那么一个可以被另一个替换。 ### V8 堆沙箱 V8 堆沙箱是一种进程内软件故障隔离,它限制源自不可信 JavaScript 或 WebAssembly 代码的内存损坏漏洞的影响范围,将其限制在进程虚拟地址空间的一个子集区域内,即 V8 堆沙箱区域。V8 堆沙箱假设攻击者可以利用来自传统 V8 漏洞(addrof 和 fakeobj)的原语,在 V8 堆沙箱区域内任意且并发地读写内存。在不失一般性的情况下,V8 堆沙箱的实现可以看作是在寻址操作上增加了一层额外的翻译。 **地址翻译** 这让你想起了操作系统课程中的地址翻译吗?V8 沙箱目前是一个 1 TB 的大区域,包含所有 V8 堆(位于沙箱起始处的 4GB V8 指针压缩笼内)、ArrayBuffer 后备存储和 Wasm 后备缓冲区。V8 堆沙箱中的寻址操作可以描述如下: - **压缩指针**:32 位指针,是 V8 堆沙箱中使用的指针表示形式。压缩指针笼分配在 V8 堆沙箱区域的起始位置。当解引用一个压缩指针时,引擎会将压缩指针笼的基址加上压缩指针,以获取 V8 堆沙箱区域中的实际地址。 - **沙箱指针**:位于沙箱内的对象可以通过一个从沙箱基址开始的 40 位偏移量来引用。 - **指针表**:V8 需要对象...

相似文章

谷歌发布影响数百万Chromium用户的利用代码

Ars Technica

谷歌发布了一个未修复的Chromium漏洞的利用代码,该漏洞可将浏览器变成一个受限的僵尸网络,影响Chrome、Edge及其他基于Chromium的浏览器。该漏洞在29个月后仍未得到修补。

2026年4月补丁星期二版本

Krebs on Security

微软2026年4月补丁星期二修复了创纪录的167个漏洞,包括一个正在被积极利用的SharePoint零日漏洞和一个公开披露的Windows Defender漏洞(BlueHammer),同时Google Chrome和Adobe Reader也修复了零日漏洞。