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, 后端开发, 数字签名, 数据可视化, 浏览器, 漏洞分析, 类型混淆, 路径探测