CVE-2025-13224
Overview
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 (
AccessorInfoorFunctionTemplateInfo), invokes the getter and returns its result. - `holder`
- the object owning the property; pre-patch typed
Union<JSReceiver, PropertyCell>, meaning it could legitimately be aPropertyCell(as for global/dictionary cells) rather than a realJSReceiver. - `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
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.
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
- Place an accessor on a cell-backed property
Arrange for a property whose backing store holds an
AccessorInfoorFunctionTemplateInfoand whoseholderreachesCallGetterIfAccessoras aPropertyCellrather than aJSReceiver. - Reach the accessor branch
Trigger the property load path so execution enters
if_accessor_infoorif_function_template_infowith thePropertyCellholder. - Cause the type confusion
Pre-patch code executes
LoadMap(holder)/LoadMapInstanceTypeorGetCreationContext(CAST(holder))on thePropertyCell, reading its fields as though they were aJSReceiver’s map/instance-type/context (type confusion). - 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
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
| Function | Change | Notes |
|---|---|---|
switchsrc/codegen/code-stub-assembler.cc |
modified | |
ifsrc/codegen/code-stub-assembler.cc |
modified |
Files Changed
src/codegen/code-stub-assembler.cc
Audit Directions
- Union-typed holders/receiversAudit CSA/builtin routines whose parameters are
Union<...>(e.g.Union<JSReceiver, PropertyCell>) and thenCASTto one arm without a runtime instance-type check; eachCASTon the untested arm is a potential type confusion. - Map/instance-type dispatch on unverified objectsReview
LoadMap/LoadMapInstanceType/GetCreationContextcalls where the object’s type is assumed rather than verified, especially in shared accessor/getter paths reachable from bothJSReceiverand cell-backed stores. - Optional/receiver-mode invariants at call sitesCheck 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.
Patch
From 62af07e96173fb03b504f60b01e67d9f2dda695b Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
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 <ishell@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
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)),
Original Bug Report
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
- https://goo.gle/bigsleep
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.cc;l=12166;drc=693f06700fd9b978f690957cde985a98463a0d87
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.cc;l=12223;drc=693f06700fd9b978f690957cde985a98463a0d87
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.h;l=4651;drc=693f06700fd9b978f690957cde985a98463a0d87
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=3404;drc=693f06700fd9b978f690957cde985a98463a0d87
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=3415;drc=693f06700fd9b978f690957cde985a98463a0d87
- https://v8.dev/docs/build