前段时间一直没有仔细研究原理, 基本就是到处抄exp加ai一把梭, 但感觉还是需要自己花时间研究学习才行, 不能太急于求成. 所以这次就花点时间研究下cve-2025-12429的原理.
这个洞属于TDZ HoleAttack的攻击面. 和之前的cve-2025-6554类似, 都是绕过v8的静态作用域分析检查

概览

  1. 作者的exp是怎么构造出越界读写的
    因为Turbofan在优化的时候把js数组的越界检查去掉了

  2. 为什么Turbofan会去掉数组检查
    因为作者构造了特殊未被检查的变量y, 被初始化为Hole, 导致Turbofan的推断出错

  3. 如何构造出未被检查的变量y?
    这是这个cve的重点, 下面慢慢分析

构造原理

还没有初始化, 但可能会在初始化前被访问的变量会在初始化前会被赋值为Hole这个特殊的值, 并且被v8标记为HoleCheckMode::kRequired. 因为Hole是不属于js语义的, 所以在访问的时候会有一个TDZ check.
TDZ(temporal dead zone)用来形容这个初始化之前, 但属于作用域的区域.

问题出在TDZ check的优化. v8会静态分析程序运行流, 对于确保已经被TDZ check过的变量, 就会消除重复检查.
所以就有了各种各样奇形怪状的控制流构造, 绕过v8的静态分析, 而一旦能访问到遗漏检查的变量, 就可以获取Hole的值.

v8里TDZ check重复的检查是以作用域为单位. 在调用HoleCheckElisionScope elider(this)时会创建一个新的HoleCheckElision作用域.
该作用域的生效范围与elider变量保持一致, 会继承最新的作用域内的HoleCheck bitmap信息, 其中记录了哪些变量被HoleCheck过. 也就是接下来生成这些变量的时候就可以省略HoleCheck.

cve-2025-12429这个漏洞是commit [7ce3a5517944fdac428313d80f8cd49474dce667]引入的. 这个commit修复了另一个bug, 却引入了一个新bug, 真是哭笑不得.

diff中看到, 作者假设的body都会执行完才到next, 所以VisitInHoleCheckElisionScope被从next的作用域移到了bodynext之前. 也就意味着body和next共享一个HoleCheckElision作用域.

1
2
for (INIT; COND; NEXT) BODY
REST
1
2
3
4
5
6
7
8
9
10
11
12
13
 void BytecodeGenerator::VisitForStatement(ForStatement* stmt) {
@@ -2486,10 +2491,11 @@ void BytecodeGenerator::VisitForStatement(ForStatement* stmt) {
// flow like breaks or continues, has its own HoleCheckElisionScope. NEXT is
// therefore conditionally evaluated and also so has its own
// HoleCheckElisionScope.
+ HoleCheckElisionScope elider(this);
VisitIterationBody(stmt, &loop_builder);
if (stmt->next() != nullptr) {
builder()->SetStatementPosition(stmt->next());
- VisitInHoleCheckElisionScope(stmt->next());
+ Visit(stmt->next());
}
}

这时候再看poc, 按照作者的代码, 由于body和next共享HoleCheckElision, 按顺序执行(没有continue时), body里面的y会先执行, 只要这个y有HoleCheck就行了, 所以use(y)处的HoleCheckElision就被消除了.
但是如果用一个continue跳过body里有HoleCheck的y, 我们就可以直接访问到没有HoleCheck的y, 破坏了作者的假设

1
2
3
4
5
6
7
8
9
10
11
12
13
function use(x) {
% DebugPrint(x);
}
function pwn() {
for (var i = 0; i < 1; use(y)) {
if (i == 0)
continue;
y;
}
let y;
}

pwn();

修复方式

修复链接
最重要的修复是这里, 给IterationBody新开了一个HoleCheckElisionScope, 并且在执行body之后将所有分支合并起来继承出来.
不再像之前那样body和next共享HoleCheckElisionScope, 而是body有自己独立作用域, 并且最终将每条分支结果的交集继承出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // Scoped class for enabling 'throw' in try-catch constructs.
@@ -3119,7 +3124,12 @@ void BytecodeGenerator::VisitIterationBody(IterationStatement* stmt,
LoopBuilder* loop_builder) {
loop_builder->LoopBody();
ControlScopeForIteration execution_control(this, stmt, loop_builder);
- Visit(stmt->body());
+ {
+ HoleCheckElisionMergeScope::Branch branch_elider(
+ execution_control.merge_elider());
+ Visit(stmt->body());
+ }
+ execution_control.merge_elider().Merge();
loop_builder->BindContinueTarget();
}