Chrome: Type confusion in v8::internal::Object::SetPropertyWithAccessor VULNERABILITY DETAILS When `SetSuperProperty` can't find the requested property in the holder, it performs an `OWN` lookup on the receiver. If the receiver has a property interceptor installed, the function invokes the interceptor's descriptor callback[1] and, if the callback opts not to intercept the request[2], creates a new property by calling `CreateDataProperty` on the original lookup iterator[3]. Some descriptor callbacks, for example, the one used with `HTMLCollection` objects, call `GetRealNamedPropertyAttributesInPrototypeChain`[4] to ensure they don't hide an existing property in the prototype chain. The function works by creating a lookup iterator that starts with the receiver's prototype and calling `GetPropertyAttributes` on it. Finally, if `GetPropertyAttributes` encounters a JavaScript Proxy object, it runs the descriptor handler of the proxy. This creates a user code reentrancy point. The problem is that the user code in the proxy handler might invalidate `own_lookup`, which is used in [3], by modifying the object, for example, by creating a property with the same name. In this scenario, `own_lookup` will be in the `NOT_FOUND` state, and `CreateDataProperty` will attempt to add a second property with the same name. This primitive has been abused multiple times in the past, including exploits detected in the wild, to break field type tracking and bypass security-critical checks in TurboFan. Therefore, last year a hardening patch was landed[6] that catches duplicate properties in \"fast property\" mode objects. We were aware of the fact that it would still be possible to create duplicate properties in \"dictionary mode\" objects with a similar vulnerability, however, even if such an object is converted to the fast mode, its field types are generalized i.e. useless for field type tracker. It turns out there's a way to exploit the duplicate property primitive without abusing field type tracking or TurboFan at all. The basic idea is that, depending on the current order of the properties, a property lookup for a duplicate property name might return a different value, and an object shape change, for example, transitioning between \"fast\" and \"dictionary\" properties, might reshuffle the properties. More specifically, when `DefineOwnPropertyIgnoreAttributes` encounters a special `AccessorInfo` property and the requested attributes don't match the current ones, it will first call `TransitionToAccessorPair`[7] to update the attributes. `TransitionToAccessorPair` might reshape the object twice if needed: first from \"fast\" to \"slow\"[9] and then back to \"fast\"[10]. After that, it restarts the lookup in the current object[11]. However, if the property is a duplicate, the new lookup might point to a different value which isn't even an accessor. This value is later used for the `SetPropertyWithAccessor` call[8]. In debug builds, this will lead to the `DCHECK_EQ(ACCESSOR, state_)` assertion failure in `LookupIterator::GetAccessors`. In release builds, a value of an attacker's choice will be interpreted as an `AccessorPair` object. REFERENCES https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/objects.cc;drc=f49e998b8d369971b65bc980846d2395bf4dee30;l=2665 ``` Maybe Object::SetSuperProperty(LookupIterator* it, Handle value, StoreOrigin store_origin, Maybe should_throw) { Isolate* isolate = it->isolate(); if (it->IsFound()) { bool found = true; Maybe result = SetPropertyInternal(it, value, should_throw, store_origin, &found); if (found) return result; } [...] Handle receiver = Handle::cast(it->GetReceiver()); // Note, the callers rely on the fact that this code is redoing the full own // lookup from scratch. LookupIterator::Configuration c = LookupIterator::OWN; LookupIterator own_lookup = it->IsElement() ? LookupIterator(isolate, receiver, it->index(), c) : LookupIterator(isolate, receiver, it->name(), c); for (; own_lookup.IsFound(); own_lookup.Next()) { switch (own_lookup.state()) { [...] case LookupIterator::INTERCEPTOR: case LookupIterator::JSPROXY: { PropertyDescriptor desc; Maybe owned = JSReceiver::GetOwnPropertyDescriptor(&own_lookup, &desc); // *** [1] *** MAYBE_RETURN(owned, Nothing()); if (!owned.FromJust()) { // *** [2] *** if (!CheckContextualStoreToJSGlobalObject(&own_lookup, should_throw)) { return Nothing(); } return JSReceiver::CreateDataProperty(&own_lookup, value, // *** [3] *** should_throw); } [...] } https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:out/Debug/gen/third_party/blink/renderer/bindings/core/v8/v8_html_collection.cc;drc=332f92aab4a32607f7813ac1a824f6ff0d86c369;l=197 ``` void V8HTMLCollection::NamedPropertyDescriptorCallback( v8::Local v8_property_name, const v8::PropertyCallbackInfo& info) { RUNTIME_CALL_TIMER_SCOPE_DISABLED_BY_DEFAULT( info.GetIsolate(), \"Blink_HTMLCollection_NamedPropertyDescriptor\"); // LegacyPlatformObjectGetOwnProperty // https://webidl.spec.whatwg.org/#LegacyPlatformObjectGetOwnProperty v8::Local v8_receiver = info.Holder(); v8::Isolate* isolate = info.GetIsolate(); v8::Local current_context = isolate->GetCurrentContext(); // step 2.1. If the result of running the named property visibility algorithm // with property name P and object O is true, then: if (v8_receiver ->GetRealNamedPropertyAttributesInPrototypeChain(current_context, // *** [4] *** v8_property_name) .IsJust()) { return; // Do not intercept. Fallback to OrdinaryGetOwnProperty. } [...] } ``` https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-objects.cc;drc=181f556032737223b6e43a48b81943b2f990daa8;l=753 ``` Maybe JSReceiver::GetPropertyAttributes( LookupIterator* it) { for (; it->IsFound(); it->Next()) { switch (it->state()) { [...] case LookupIterator::JSPROXY: return JSProxy::GetPropertyAttributes(it); // *** [5] *** [...] ``` [6] - https://source.chromium.org/chromium/_/chromium/v8/v8.git/+/3c7f274770e90b766ed554a6ca599e70341c9735 https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-objects.cc;drc=181f556032737223b6e43a48b81943b2f990daa8;l=3650 ``` Maybe JSObject::DefineOwnPropertyIgnoreAttributes( LookupIterator* it, Handle value, PropertyAttributes attributes, Maybe should_throw, AccessorInfoHandling handling, EnforceDefineSemantics semantics, StoreOrigin store_origin) { [...] case LookupIterator::ACCESSOR: { Handle accessors = it->GetAccessors(); // Special handling for AccessorInfo, which behaves like a data // property. if (accessors->IsAccessorInfo() && handling == DONT_FORCE_FIELD) { PropertyAttributes current_attributes = it->property_attributes(); // Ensure the context isn't changed after calling into accessors. AssertNoContextChange ncc(it->isolate()); // Update the attributes before calling the setter. The setter may // later change the shape of the property. if (current_attributes != attributes) { it->TransitionToAccessorPair(accessors, attributes); // *** [7] *** } return Object::SetPropertyWithAccessor(it, value, should_throw); // *** [8] *** } ``` https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:v8/src/objects/lookup.cc;drc=332f92aab4a32607f7813ac1a824f6ff0d86c369;l=815 ``` void LookupIterator::TransitionToAccessorPair(Handle pair, PropertyAttributes attributes) { Handle receiver = GetStoreTarget(); holder_ = receiver; PropertyDetails details(PropertyKind::kAccessor, attributes, PropertyCellType::kMutable); if (IsElement(*receiver)) { [...] } else { PropertyNormalizationMode mode = CLEAR_INOBJECT_PROPERTIES; if (receiver->map(isolate_).is_prototype_map()) { JSObject::InvalidatePrototypeChains(receiver->map(isolate_)); mode = KEEP_INOBJECT_PROPERTIES; } // Normalize object to make this operation simple. JSObject::NormalizeProperties(isolate_, receiver, mode, 0, // *** 9 *** \"TransitionToAccessorPair\"); JSObject::SetNormalizedProperty(receiver, name_, pair, details); JSObject::ReoptimizeIfPrototype(receiver); // *** [10] *** ReloadPropertyInformation(); // *** [11] *** } } ``` VERSION Google Chrome 112.0.5615.137 (Official Build) (arm64) Chromium 115.0.5737.0 (Developer Build) (64-bit) REPRODUCTION CASE ``` ``` CREDIT INFORMATION Sergei Glazunov of Google Project Zero This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2023-07-26. Related CVE Numbers: CVE-2023-2935. Found by: glazunov@google.com