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

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Sub-componentIC
Bug ClassType Confusion
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
The mapped commit may not contain the whole fix.
Reviewer notes: Diff is truncated, so the callers that passed a PropertyCell are not shown. The AccessorInfo branch's instance-type dispatch would send a PropertyCell to if_bailout, so the confusion lies only in the FunctionTemplateInfo CAST/GetCreationContext path. The commit is titled a cleanup.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker450328966
Fix commit62af07e96173 (v8/v8) +142/-108
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle Big Sleep
Reported2025-10-09 (per vendor advisory)
Fix landed2025-10-13 (public git)
Disclosed2025-11-17
Patch gap35 days — fix public in source before the advisory

Background

`CodeStubAssembler::CallGetterIfAccessor`
a CSA routine that, given a property’s backing-store contents, either returns the value directly or, if it is an accessor (AccessorInfo or FunctionTemplateInfo), invokes the getter and returns its result.
`holder`
the object owning the property; pre-patch typed Union<JSReceiver, PropertyCell>, meaning it could legitimately be a PropertyCell (as for global/dictionary cells) rather than a real JSReceiver.
`AccessorInfo` / `FunctionTemplateInfo`
internal accessor descriptors; handling them requires reading the holder’s map/instance type and computing a creation context, operations only valid on a JSReceiver.
`ExpectedReceiverMode`
enum { kExpectingJSReceiver, kExpectingAnyReceiver } selecting how the receiver for the getter call is derived.

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: Diff is truncated, so the callers that passed a PropertyCell are not shown. The AccessorInfo branch's instance-type dispatch would send a PropertyCell to if_bailout, so the confusion lies only in the FunctionTemplateInfo CAST/GetCreationContext path. The commit is titled a cleanup.

CallGetterIfAccessor accepted holder as Union<JSReceiver, PropertyCell>, but in both the if_function_template_info and if_accessor_info branches it treated holder unconditionally as a JSReceiver: it did CAST(holder) to a JSReceiver, called GetCreationContext(holder_receiver, ...), and LoadMap(holder)/LoadMapInstanceType to dispatch on JSArray/JSFunction/JSPrimitiveWrapper instance types. When the property actually lived on a PropertyCell, those operations reinterpret a PropertyCell’s fields as those of a JSReceiver, a type confusion.

The patch retypes holder as std::optional<TNode<JSReceiver>>, makes expected_receiver_mode a mandatory, properly-computed parameter, and guards the accessor branches with if (holder.has_value()); when a holder is absent (i.e. not a JSReceiver), the FunctionTemplateInfo path takes Unreachable() and the AccessorInfo path is likewise gated, so a PropertyCell is never dispatched on as a JSReceiver. The dictionary call site is updated to pass kExpectingJSReceiver explicitly, ensuring the receiver mode matches the holder actually supplied.

Key insight
The core mistake was allowing a PropertyCell to be passed and then treated as a JSReceiver (map/instance-type dispatch, GetCreationContext) inside CallGetterIfAccessor; the fix narrows holder to an optional JSReceiver and makes the non-JSReceiver case Unreachable/bailout instead of confused.

Attack Path

  1. Place an accessor on a cell-backed property Arrange for a property whose backing store holds an AccessorInfo or FunctionTemplateInfo and whose holder reaches CallGetterIfAccessor as a PropertyCell rather than a JSReceiver.
  2. Reach the accessor branch Trigger the property load path so execution enters if_accessor_info or if_function_template_info with the PropertyCell holder.
  3. Cause the type confusion Pre-patch code executes LoadMap(holder)/LoadMapInstanceType or GetCreationContext(CAST(holder)) on the PropertyCell, reading its fields as though they were a JSReceiver’s map/instance-type/context (type confusion).
  4. Leverage the mismatch The misinterpreted fields drive instance-type dispatch or a creation-context load on the wrong object shape, giving an attacker a primitive from the confusion (inferred).

Impact Assessment

An attacker running crafted JavaScript in the renderer can make CallGetterIfAccessor treat a PropertyCell as a JSReceiver, a type confusion in the V8 sandbox that can be steered via the accessor and receiver handling (inferred). The advisory classifies this as Type Confusion in V8 and does not report exploitation in the wild; escalation beyond the type confusion the patch prevents is (inferred). The precondition is triggering an accessor (AccessorInfo/FunctionTemplateInfo) load whose holder is a PropertyCell on the affected CSA path.

Changed Functions

FunctionChangeNotes
switch
src/codegen/code-stub-assembler.cc
modified
if
src/codegen/code-stub-assembler.cc
modified

Files Changed

  • src/codegen/code-stub-assembler.cc

Audit Directions

  • Union-typed holders/receivers
    Audit CSA/builtin routines whose parameters are Union<...> (e.g. Union<JSReceiver, PropertyCell>) and then CAST to one arm without a runtime instance-type check; each CAST on the untested arm is a potential type confusion.
  • Map/instance-type dispatch on unverified objects
    Review LoadMap/LoadMapInstanceType/GetCreationContext calls where the object’s type is assumed rather than verified, especially in shared accessor/getter paths reachable from both JSReceiver and cell-backed stores.
  • Optional/receiver-mode invariants at call sites
    Check callers of routines with newly mandatory mode parameters (ExpectedReceiverMode) to confirm the supplied holder actually satisfies the declared expectation, since a mismatched mode reintroduces the confusion.
From 62af07e96173fb03b504f60b01e67d9f2dda695b Mon Sep 17 00:00:00 2001
From: Igor Sheludko <[email protected]>
Date: Mon, 13 Oct 2025 14:22:20 +0200
Subject: [PATCH] [ic] Cleanup AccessorAssembler::CallGetterIfAccessor()

This CL
 - reorders parameters to make |expected_receiver_mode| a mandatory
   one and properly computed,
 - makes sure we don't pass PropertyCell as a holder when JSReceiver is
   expected.

Bug: 450328966
Change-Id: I921dfbd99245d01143600b4f4713fe602c817657
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7036691
Commit-Queue: Igor Sheludko <[email protected]>
Reviewed-by: Leszek Swirski <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103085}
---

diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index a8e0c85..b749310 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -11662,7 +11662,8 @@
 
               var_value = CallGetterIfAccessor(
                   value_or_accessor, object, var_details.value(), context,
-                  object, next_key, &slow_load, kCallJSGetterUseCachedName);
+                  object, kExpectingJSReceiver, next_key, &slow_load,
+                  kCallJSGetterUseCachedName);
               Goto(&value_ready);
 
               BIND(&slow_load);
@@ -12158,15 +12159,11 @@
     TNode<SwissNameDictionary> dictionary, TNode<IntPtrT> name_index,
     TVariable<Uint32T>* var_details, TVariable<Object>* var_value);
 
-// |value| is the property backing store's contents, which is either a value or
-// an accessor pair, as specified by |details|. |holder| is a JSReceiver or a
-// PropertyCell. Returns either the original value, or the result of the getter
-// call.
 TNode<Object> CodeStubAssembler::CallGetterIfAccessor(
-    TNode<Object> value, TNode<Union<JSReceiver, PropertyCell>> holder,
+    TNode<Object> value, std::optional<TNode<JSReceiver>> holder,
     TNode<Uint32T> details, TNode<Context> context, TNode<JSAny> receiver,
-    TNode<Object> name, Label* if_bailout, GetOwnPropertyMode mode,
-    ExpectedReceiverMode expected_receiver_mode) {
+    ExpectedReceiverMode expected_receiver_mode, TNode<Object> name,
+    Label* if_bailout, GetOwnPropertyMode mode) {
   TVARIABLE(Object, var_value, value);
   Label done(this), if_accessor_info(this, Label::kDeferred);
 
@@ -12207,44 +12204,51 @@
 
       BIND(&if_function_template_info);
       {
-        Label use_cached_property(this);
-        TNode<HeapObject> cached_property_name = LoadObjectField<HeapObject>(
-            getter, FunctionTemplateInfo::kCachedPropertyNameOffset);
+        if (holder.has_value()) {
+          Label use_cached_property(this);
+          TNode<HeapObject> cached_property_name = LoadObjectField<HeapObject>(
+              getter, FunctionTemplateInfo::kCachedPropertyNameOffset);
 
-        Label* has_cached_property = mode == kCallJSGetterUseCachedName
-                                         ? &use_cached_property
-                                         : if_bailout;
-        GotoIfNot(IsTheHole(cached_property_name), has_cached_property);
+          Label* has_cached_property = mode == kCallJSGetterUseCachedName
+                                           ? &use_cached_property
+                                           : if_bailout;
+          GotoIfNot(IsTheHole(cached_property_name), has_cached_property);
 
-        TNode<JSReceiver> js_receiver;
-        switch (expected_receiver_mode) {
-          case kExpectingJSReceiver:
-            js_receiver = CAST(receiver);
-            break;
-          case kExpectingAnyReceiver:
-            // TODO(ishell): in case the function template info has a signature
-            // and receiver is not a JSReceiver the signature check in
-            // CallFunctionTemplate builtin will fail anyway, so we can short
-            // cut it here and throw kIllegalInvocation immediately.
-            js_receiver = ToObject_Inline(context, receiver);
-            break;
-        }
-        TNode<JSReceiver> holder_receiver = CAST(holder);
-        TNode<NativeContext> creation_context =
-            GetCreationContext(holder_receiver, if_bailout);
-        TNode<Context> caller_context = context;
-        var_value = CallBuiltin(
-            Builtin::kCallFunctionTemplate_Generic, creation_context, getter,
-            Int32Constant(i::JSParameterCount(0)), caller_context, js_receiver);
-        Goto(&done);
-
-        if (mode == kCallJSGetterUseCachedName) {
-          Bind(&use_cached_property);
-
-          var_value =
-              GetProperty(context, holder_receiver, cached_property_name);
-
+          TNode<JSReceiver> js_receiver;
+          switch (expected_receiver_mode) {
+            case kExpectingJSReceiver:
+              js_receiver = CAST(receiver);
+              break;
+            case kExpectingAnyReceiver:
+              // TODO(ishell): in case the function template info has a
+              // signature and receiver is not a JSReceiver the signature check
+              // in CallFunctionTemplate builtin will fail anyway, so we can
+              // short cut it here and throw kIllegalInvocation immediately.
+              js_receiver = ToObject_Inline(context, receiver);
+              break;
+          }
+          TNode<JSReceiver> holder_receiver = *holder;
+          TNode<NativeContext> creation_context =
+              GetCreationContext(holder_receiver, if_bailout);
+          TNode<Context> caller_context = context;
+          var_value = CallBuiltin(Builtin::kCallFunctionTemplate_Generic,
+                                  creation_context, getter,
+                                  Int32Constant(i::JSParameterCount(0)),
+                                  caller_context, js_receiver);
           Goto(&done);
+
+          if (mode == kCallJSGetterUseCachedName) {
+            Bind(&use_cached_property);
+
+            var_value =
+                GetProperty(context, holder_receiver, cached_property_name);
+
+            Goto(&done);
+          }
+        } else {
+          // |holder| must be available in order to handle lazy AccessorPair
+          // case (we need it for computing the function's context).
+          Unreachable();
         }
       }
     } else {
@@ -12256,56 +12260,61 @@
   // AccessorInfo case.
   BIND(&if_accessor_info);
   {
-    TNode<AccessorInfo> accessor_info = CAST(value);
-    Label if_array(this), if_function(this), if_wrapper(this);
+    if (holder.has_value()) {
+      TNode<AccessorInfo> accessor_info = CAST(value);
+      Label if_array(this), if_function(this), if_wrapper(this);
+      // Dispatch based on {holder} instance type.
+      TNode<Map> holder_map = LoadMap(*holder);
+      TNode<Uint16T> holder_instance_type = LoadMapInstanceType(holder_map);
+      GotoIf(IsJSArrayInstanceType(holder_instance_type), &if_array);
+      GotoIf(IsJSFunctionInstanceType(holder_instance_type), &if_function);
+      Branch(IsJSPrimitiveWrapperInstanceType(holder_instance_type),
+             &if_wrapper, if_bailout);
 
-    // Dispatch based on {holder} instance type.
-    TNode<Map> holder_map = LoadMap(holder);
-    TNode<Uint16T> holder_instance_type = LoadMapInstanceType(holder_map);
-    GotoIf(IsJSArrayInstanceType(holder_instance_type), &if_array);
-    GotoIf(IsJSFunctionInstanceType(holder_instance_type), &if_function);
-    Branch(IsJSPrimitiveWrapperInstanceType(holder_instance_type), &if_wrapper,
-           if_bailout);
+      // JSArray AccessorInfo case.
+      BIND(&if_array);
+      {
+        // We only deal with the "length" accessor on JSArray.
+        GotoIfNot(IsLengthString(LoadObjectField(accessor_info,
+                                                 AccessorInfo::kNameOffset)),
+                  if_bailout);
+        TNode<JSArray> array = CAST(*holder);
+        var_value = LoadJSArrayLength(array);
+        Goto(&done);
+      }
 
-    // JSArray AccessorInfo case.
-    BIND(&if_array);
-    {
-      // We only deal with the "length" accessor on JSArray.
-      GotoIfNot(IsLengthString(
-                    LoadObjectField(accessor_info, AccessorInfo::kNameOffset)),
-                if_bailout);
-      TNode<JSArray> array = CAST(holder);
-      var_value = LoadJSArrayLength(array);
-      Goto(&done);
-    }
+      // JSFunction AccessorInfo case.
+      BIND(&if_function);
+      {
+        // We only deal with the "prototype" accessor on JSFunction here.
+        GotoIfNot(IsPrototypeString(LoadObjectField(accessor_info,
+                                                    AccessorInfo::kNameOffset)),
+                  if_bailout);
 
-    // JSFunction AccessorInfo case.
-    BIND(&if_function);
-    {
-      // We only deal with the "prototype" accessor on JSFunction here.
-      GotoIfNot(IsPrototypeString(
-                    LoadObjectField(accessor_info, AccessorInfo::kNameOffset)),
Loading diff…

Original Bug Report

reported by [email protected]

V8: Type Confusion in LoadSuperIC

We are tracking this issue with the public ID BIGSLEEP-450458037. Please use this identifier for reference in any future communication.

Vulnerability Details

There is a type confusion bug in the handling of super property loads with accessors.

The function CodeStubAssembler::CallGetterIfAccessor [1] is used for example by ICs to invoke a getter function for a property. One of its parameters, expected_receiver_mode, indicates if the function can assume the receiver to be a JSReceiver or if it may need to perform a ToObject conversion. By default, the value is kExpectingJSReceiver [2] and so the function will assume that the receiver is always a JSReceiver instance. As such, all callers to this function that don’t pass this parameter need to guarantee that this is the case.

One place where this goes wrong is in the handling of super property accesses, specifically in the LoadSuperIC [3]. Here, the (generic) property lookup will not be performed starting from the receiver, but instead start at the lookup_start_object [4], which will be the prototype of the instance type of the current class. However, in case of an accessor function, the accessor will be invoked with the actual receiver [5]. As such, it becomes possible for the lookup to find and invoke an accessor function with an invalid receiver (e.g. a Smi).

The below testcase demonstrates this and causes a DCHECK failure in debug builds and a segmentation fault in release builds.

[1] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.cc;l=12166;drc=693f06700fd9b978f690957cde985a98463a0d87
[2] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.h;l=4651;drc=693f06700fd9b978f690957cde985a98463a0d87
[3] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=3404;drc=693f06700fd9b978f690957cde985a98463a0d87
[4] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=3415;drc=693f06700fd9b978f690957cde985a98463a0d87
[5] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.cc;l=12223;drc=693f06700fd9b978f690957cde985a98463a0d87

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit 1370b0bd0727045ac742c2a8bec77ccb3fdcc105)
  • in stable release 14.1.146.11 (commit ad8af0fc661d278e87627fcaa3a7cf795ee80dd8)

Reproduction

Test Case

// Create an Error instance. Error instances have an own property 'stack'
// which is an accessor backed by a FunctionTemplateInfo (API accessor).
const err = new Error();

class B {
  m() {
    // Access 'stack' via super.
    // The lookup starts at the prototype of B.prototype.
    return super.stack;
  }
}

// Set the prototype of B.prototype to the Error instance.
// Now, the super lookup will start at 'err'.
Object.setPrototypeOf(B.prototype, err);

const b = new B();

// Call the method m with a primitive receiver.
// This triggers LoadSuperIC and ultimately calls CallGetterIfAccessor with a
// Smi as receiver and the default kExpectingJSReceiver mode.
b.m.call(0x4141414 >> 1);

Build Instructions

Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:

gm.py x64.debug

Command

./out/x64.debug/d8 crash.js

ASan Report

#
# Fatal error in ../../src/objects/object-type.cc, line 87
# Type cast failed in CAST(receiver) at ../../src/codegen/code-stub-assembler.cc:12223
  Expected JSReceiver but found Smi: 0x20a0a0a (34212362)
#
#
#
#FailureMessage Object: 0x7b8dd8b89060
==== C stack trace ===============================
    ./out/x64.debug/d8(___interceptor_backtrace+0x46) [0x55806b2944c6]
    v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7f8df467192e]
    v8/v8/out/x64.debug/libv8_libplatform.so(+0x7e0fb) [0x7f8ddc7ba0fb]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x2e2) [0x7f8df4617052]
    v8/v8/out/x64.debug/libv8.so(v8::internal::CheckObjectType(unsigned long, unsigned long, unsigned long)+0x10ed5) [0x7f8dea2bfc85]
    [0x7b8d7c8e911d]

Reporter Credit

Google Big Sleep

Disclosure Policy

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 2026-01-07. For more information, visit https://goo.gle/bigsleep

View on issue tracker