文章首发于安全KER https://www.anquanke.com/post/id/234429
0x00 前言
Issue 941743是2019年的一个v8方面的历史漏洞,其漏洞发生在对Array.prototype.map函数的Reduce过程,之前介绍过Array.prototype.map的一个回调漏洞,本文将介绍其在JIT层的一个优化漏洞。
0x01 前置知识
Array.prototype.map()
Array.prototype.map()函数用于从一个数组中根据函数关系创建一个映射,其语法如下
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
基本用法如下
1 | var a = [1,2,3]; |
输出如下
1 | index=0 value=1 |
Array()函数调用链及JIT优化分析
源码分析
当我们执行var a = Array(1)时,首先调用的是ArrayConstructor,该函数位于src/builtins/builtins-array-gen.cc,按照源码分析,其调用链为ArrayConstructor -> ArrayConstructorImpl -> GenerateArrayNArgumentsConstructor -> TailCallRuntime
GenerateArrayNArgumentsConstructor函数如下,其结尾使用了TailCallRuntime去调用某个函数
1 | void ArrayBuiltinsAssembler::GenerateArrayNArgumentsConstructor( |
而TailCallRuntime函数在不同指令架构上有不同的实现,这里我们看x64架构的实现
1 | void MacroAssembler::TailCallRuntime(Runtime::FunctionId fid) { |
通过Runtime::FunctionForId(fid)找到函数对象,在源码文件中src/runtime/runtime.cc中有定义
1 | const Runtime::Function* Runtime::FunctionForId(Runtime::FunctionId id) { |
其中kIntrinsicFunctions的定义如下
1 | static const Runtime::Function kIntrinsicFunctions[] = { |
宏定义FOR_EACH_INTRINSIC如下
1 |
其中,我们较为关注的kNewArray函数在FOR_EACH_INTRINSIC_ARRAY里被注册
1 | #define FOR_EACH_INTRINSIC_ARRAY(F, I) \ |
由此可以知道Array(1)最终调用的是NewArray函数,该函数位于src/runtime/runtime-array.cc文件
1 | RUNTIME_FUNCTION(Runtime_NewArray) { |
以上代码,仅保留了我们较为关注的地方,从中可以看出,如果数组元素类型为Smi类型,并且value >= JSArray::kInitialMaxFastElementArray成立,也就是数组长度大于JSArray::kInitialMaxFastElementArray值的时候,can_inline_array_constructor被标记为false,最终,因为该标记,site->SetDoNotInlineCall()函数被调用。该标记最终将会在src/compiler/js-create-lowering.cc文件中的ReduceJSCreateArray函数中使用
1 | if (length_type.Maybe(Type::UnsignedSmall()) && can_inline_call) { |
从分析中可以看出,该标记将影响Array(1)这个函数在JIT编译时是否会被内联优化。
IR图分析
首先,我们的测试代码如下,需要知道的一点是Array.prototype.map内部会调用JSCreateArray来创建新数组存放结果
1 | //将can_inline_array_constructor设置为false |
其IR图如下,可以看到,在typed lowering阶段,JSCreateArray并没有被优化为JIT代码,其仍然为JS层的代码调用。

接下来,我们去除测试脚本里的Array(2**30);这句,然后重新查看IR图,可以发现,其被优化成了本地代码了。

0x02 漏洞分析
patch分析
1 | diff --git a/src/compiler/js-call-reducer.cc b/src/compiler/js-call-reducer.cc |
该patch用于修复漏洞,patch位于src/compiler/js-call-reducer.cc文件中的JSCallReducer::ReduceArrayMap函数,该函数是对Array.prototype.map函数进行优化的,patch中主要增加了一个对源数组的长度进行检查,检查其是否大于kMaxFastArrayLength,因为添加的是一个CheckBounds节点,所以如果大于的话将deoptimization bailout从而不使用其生成的JIT代码。
我们来分析一下代码
1 | Node* original_length = effect = graph()->NewNode( |
以上代码看似没有什么问题,但忽略了JSCreateArray的一个特性,如果要申请的大小大于某个阈值(0x2000000),那么其返回的对象,其Element不再是数组类型,而是Dictionary类型,测试代码
1 | var a = Array(0x2000001); |
当JSCreateArray返回的是Dictionary类型时,V8的优化代码仍然是以数组连续的方式写值的。在就导致了数组溢出。
POC
1 | Array(2**30); |
调试过程,报了Segmentation fault.错误,这是因为越界写,超过了可访问的区域。
1 | Thread 1 "d8" received signal SIGSEGV, Segmentation fault. |
疑难问题
我们还注意到一个细节,我们的数组是HOLEY_SMI_ELEMENTS,首先,SMI是为了满足JSCreateArray不内联的条件,而HOLEY是为了能够溢出方便控制内存,因为空洞的原因,不会对某块区域进行写,从而不至于破坏内存中其他地方,仅去覆盖我们需要的地方。
1 | var a = [1,2,3,,,,4]; |
另一个问题是为何要防止JSCreateArray内联,首先,我们去除开头的Array(2**30),然后观察IR图。没内联时是这样的

内联以后是这样的,因为内联多了个CheckBound,且我们触发漏洞的length显然超过这个范围,这将导致直接deoptimization bailout。

gdb调试如下
1 | 0x00003bbfbc0830fb in ?? () |
可以看到,因为cmp r8d, 0x7ff8比较不通过导致直接deoptimization bailout了,因此JSCreateArray不能内联。
exp
通过溢出,覆盖Array的length,从而构造一个能自由控制的oob数组,然后就很容易利用了,当我们完成构造oob数组以后,我们使用throw抛出一个异常,从而可以使得map函数停止向后的迭代。
1 | var buf = new ArrayBuffer(0x8); |
0x03 感想
通过本次实践,对于V8的知识又增加了,还得不断的学习。
0x04 参考
Array.prototype.map()
把握机会之窗:看我如何获得Chrome 1-day漏洞并实现利用
Chrome M73 issue 941743