crixpwn/CVE-2026-8389
GitHub: crixpwn/CVE-2026-8389
该项目是对 SpiderMonkey BaselineJIT 中 `pcOffset` 位域截断导致类型混淆漏洞(CVE-2026-8389)的详细技术分析与概念验证。
Stars: 40 | Forks: 6
# CVE-2026-8389
此漏洞原计划在 2026 年柏林 Pwn2Own 大赛上使用,但已在版本 150.0.3 中被修复。
SpiderMonkey BaselineJIT `pcOffset` 位域在 eager off-thread baseline-compile 路径中发生截断,导致在异常展开期间出现错误但在边界内的 bytecode pc,并随后引发类型混淆。
## 概述
`RetAddrEntry` 将 bytecode 偏移量 `pcOffset_` 存储为 28 位位域。在同一头文件中定义的 `BaselineMaxScriptLength = 0x0fffffff` 常量的大小正好与该位域范围相匹配。
```
// js/src/jit/BaselineJIT.h:73
static constexpr uint32_t BaselineMaxScriptLength = 0x0fffffffu;
// js/src/jit/BaselineJIT.h:100-105
class RetAddrEntry {
// Offset from the start of the JIT code where call instruction is.
uint32_t returnOffset_;
// The offset of this bytecode op within the JSScript.
uint32_t pcOffset_ : 28;
```
```
// js/src/jit/BaselineJIT.h:141-156 (RetAddrEntry constructor)
RetAddrEntry(uint32_t pcOffset, Kind kind, CodeOffset retOffset)
: returnOffset_(uint32_t(retOffset.offset())),
pcOffset_(pcOffset),
kind_(uint32_t(kind)) {
MOZ_ASSERT(returnOffset_ == retOffset.offset(),
"retOffset must fit in returnOffset_");
// The pc offset must fit in at least 28 bits, since we shave off 4 for
// the Kind enum.
MOZ_ASSERT(pcOffset_ == pcOffset);
static_assert(BaselineMaxScriptLength <= (1u << 28) - 1);
MOZ_ASSERT(pcOffset <= BaselineMaxScriptLength);
MOZ_ASSERT(kind < Kind::Invalid);
MOZ_ASSERT(this->kind() == kind, "kind must fit in kind_ bit field");
}
```
在修复之前,该上限仅在 release 构建版本的 `CanEnterBaselineJIT`(`js/src/jit/BaselineJIT.cpp`)内部被执行,这是预热 / OSR 主线程入口路径。eager off-thread baseline-compile 路径并未经过该检查,因此 bytecode 超过 256 MB 的脚本可以通过 baseline 编译,而其 `pcOffset` 会在 28 位存储(`pcOffset & 0x0FFFFFFF`)中被静默截断。上述两个信息性 `MOZ_ASSERT`(`pcOffset_ == pcOffset` 和 `pcOffset <= BaselineMaxScriptLength`)在 release 构建版本中是空操作(no-op),因此截断未被发现。
## 受影响路径
eager off-thread baseline-compile 路径:
```
CompilationStencil::instantiateStencils
-> MaybeDoEagerBaselineCompilations (js/src/frontend/Stencil.cpp:2720)
-> DispatchOffThreadBaselineBatchEager (js/src/jit/BaselineJIT.cpp:386)
-> BaselineCompileTask::runTask (js/src/jit/BaselineCompileTask.cpp:69)
-> BaselineCompile
```
修复前,`MaybeDoEagerBaselineCompilations` 仅依赖于 `script->baselineDisabled()` 和 `jit::CanBaselineInterpretScript(script)` 进行限制。这两者都不会验证脚本长度,因此过长的脚本能够通过此路径进入 baseline 编译。
```
// js/src/frontend/Stencil.cpp, MaybeDoEagerBaselineCompilations (pre-patch)
if (script->baselineDisabled()) {
continue;
}
if (!jit::CanBaselineInterpretScript(script)) {
continue;
}
```
相比之下,主线程路径强制执行了边界(此代码块后来被移至共享的 `CanBaselineCompileScript` 中):
```
// js/src/jit/BaselineJIT.cpp, CanEnterBaselineJIT (pre-patch)
if (script->length() > BaselineMaxScriptLength) {
script->disableBaselineCompile();
return Method_CantCompile;
}
```
## 后果
被截断的 `pcOffset` 在 `JSJitFrameIter::baselineScriptAndPc` 中通过 `RetAddrEntry::pc` -> `JSScript::offsetToPC` 被转换回 bytecode 指针。
```
// js/src/jit/JSJitFrameIter.cpp:155-160
// address.
uint8_t* retAddr = resumePCinCurrentFrame();
const RetAddrEntry& entry =
script->baselineScript()->retAddrEntryFromReturnAddress(retAddr);
*pcRes = entry.pc(script);
}
```
```
// js/src/jit/BaselineJIT.h:162-164 (RetAddrEntry::pc)
jsbytecode* pc(JSScript* script) const {
return script->offsetToPC(pcOffset_);
}
```
该 pc 被传递给异常处理程序 `HandleExceptionBaseline`。由于被截断的偏移量小于实际脚本长度,`offsetToPC` 返回的是一个在边界内但错误的 pc,因此该故障是静默的,而不是明显的超出范围崩溃。
```
// js/src/jit/JitFrames.cpp:584-591
static void HandleExceptionBaseline(JSContext* cx, JSJitFrameIter& frame,
CommonFrameLayout* prevFrame,
ResumeFromException* rfe) {
MOZ_ASSERT(frame.isBaselineJS());
MOZ_ASSERT(prevFrame);
jsbytecode* pc;
frame.baselineScriptAndPc(nullptr, &pc);
```
该错误的 pc 会在异常展开期间导致错误的 try-note 匹配(`HandleExceptionBaseline` 以 `script->trynotes()` 为键),进而导致错误的栈槽读取。一个非 `JSObject*` 类型的槽可能会被当作该类型处理,并通过其 vtable 进行分发,从而产生作为进一步利用基础的类型混淆。
标签:CVE, Go语言工具, SpiderMonkey, 后端开发, 数字签名, 数据可视化, 浏览器, 漏洞分析, 类型混淆, 路径探测