前段时间一直没有仔细研究原理, 基本就是到处抄exp加ai一把梭, 但感觉还是需要自己花时间研究学习才行, 不能太急于求成. 所以这次就花点时间研究下cve-2025-12429的原理.
这个洞属于TDZ HoleAttack的攻击面. 和之前的cve-2025-6554类似, 都是绕过v8的静态作用域分析检查
概览
作者的exp是怎么构造出越界读写的
因为Turbofan在优化的时候把js数组的越界检查去掉了为什么Turbofan会去掉数组检查
因为作者构造了特殊未被检查的变量y, 被初始化为Hole, 导致Turbofan的推断出错如何构造出未被检查的变量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的作用域移到了body和next之前. 也就意味着body和next共享一个HoleCheckElision作用域.
1 | for (INIT; COND; NEXT) BODY |
1 | void BytecodeGenerator::VisitForStatement(ForStatement* stmt) { |
这时候再看poc, 按照作者的代码, 由于body和next共享HoleCheckElision, 按顺序执行(没有continue时), body里面的y会先执行, 只要这个y有HoleCheck就行了, 所以use(y)处的HoleCheckElision就被消除了.
但是如果用一个continue跳过body里有HoleCheck的y, 我们就可以直接访问到没有HoleCheck的y, 破坏了作者的假设
1 | function use(x) { |
修复方式
修复链接
最重要的修复是这里, 给IterationBody新开了一个HoleCheckElisionScope, 并且在执行body之后将所有分支合并起来继承出来.
不再像之前那样body和next共享HoleCheckElisionScope, 而是body有自己独立作用域, 并且最终将每条分支结果的交集继承出来
1 | // Scoped class for enabling 'throw' in try-catch constructs. |