High patch-now ✔︎ verified 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-componentMaglev
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — 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 AIConfirmed
Reviewer notes: Mechanism matches the commit message and guard. Minor: the downstream code that misuses the non-JSReceiver receiver is not in the diff, so the 'treated as array JSReceiver' consequence is inferred.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker456547591
Fix commit00348ef5c3c1 (v8/v8) +4/-0
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
CreditedShreyas Penkar (@streypaws)
Reported2025-10-31 · bounty $11000 (per vendor advisory)
Fix landed2025-10-31 (public git)
Disclosed2025-12-02
Patch gap32 days — fix public in source before the advisory

Background

Maglev
V8’s mid-tier optimizing compiler that builds a graph of typed ValueNodes from bytecode and applies fast-path reductions for common builtins.
`NodeType::kJSReceiver`
a Maglev node-type lattice value asserting that a value is a JSReceiver (i.e. an object, not a primitive like a Number or String).
`CheckType`
a helper that queries a node’s statically known NodeType and returns whether the node is provably of the requested type.
`TryGetPossibleMaps`
a known_node_aspects() query returning the set of maps a receiver may have, used by builtin reducers to specialize on object shape.

Root Cause Analysis

The reducer for Array.prototype.slice in src/maglev/maglev-graph-builder.cc (the enclosing function name is not present in the @@ hunk header, but the commit subject identifies it as the Array.p.slice reduction) proceeded to call known_node_aspects().TryGetPossibleMaps(receiver) and specialize on the resulting maps without first establishing that receiver is actually a JSReceiver. As the commit message states, the code could be reached in a polymorphic branch where the receiver is surely not a JSReceiver, yet this fact is not derivable from the set of possible maps, so TryGetPossibleMaps could return maps (or otherwise let the reducer continue) while the value was in truth a primitive. This violates the invariant that an array builtin fast path only operates on genuine JSReceiver/JSArray receivers, yielding a type confusion.

The fix adds an explicit if (!CheckType(receiver, NodeType::kJSReceiver)) { return {}; } guard that runs before TryGetPossibleMaps, bailing out of the reduction (returning an empty ReduceResult) whenever the receiver is not provably a JSReceiver, forcing a generic call instead of the specialized path.

Key insight
The reducer trusted map information as a proxy for object-ness, but possible-map data does not encode the case where a value is known to be a non-JSReceiver; the fix inserts a direct CheckType(receiver, NodeType::kJSReceiver) test so the fast path is entered only when the receiver is provably an object.

Attack Path

  1. Reach the slice reducer polymorphically Craft JavaScript that calls Array.prototype.slice in a polymorphic call site so Maglev attempts to reduce it with a receiver whose static type includes a non-JSReceiver possibility.
  2. Evade the map-based check Arrange the site so the receiver is actually a primitive in a branch that the possible-maps analysis does not exclude, letting the old code skip past the missing type check.
  3. Enter the specialized fast path Without the guard, the reducer continues to TryGetPossibleMaps and emits slice code that assumes a JSReceiver/array layout for a value that is not one, producing a type-confused node graph.
  4. Trigger the confusion at runtime Execute the optimized code so the mis-typed access operates on a primitive as if it were an array object (inferred: the specific confused access/OOB sink is not shown in the diff).

Impact Assessment

An attacker running untrusted JavaScript in the renderer can drive Maglev into emitting a type-confused fast path for Array.prototype.slice, treating a non-object receiver as an array JSReceiver. This is a type confusion in V8 (the advisory classifies it as Type Confusion in V8); any further memory read/write or exploitation primitive is (inferred) as the patch itself demonstrates only the missing type guard. The advisory does not state exploited-in-the-wild status here; preconditions are the ability to execute script and to shape a call site so Maglev optimizes the vulnerable reduction.

Changed Functions

FunctionChangeNotes
if
src/maglev/maglev-graph-builder.cc
modified

Files Changed

  • src/maglev/maglev-graph-builder.cc

Audit Directions

  • Map info used as a type proxy
    Audit other builtin reducers that call TryGetPossibleMaps (or otherwise branch on possible maps) without a preceding CheckType(..., kJSReceiver); possible-map sets do not encode a proven non-JSReceiver case.
  • Polymorphic receiver assumptions
    Look for fast paths that assume the receiver is an object in polymorphic branches, especially array/this-based builtins reduced in Maglev.
  • Reducer early-exit ordering
    Verify that type-establishing guards run before any map- or shape-dependent specialization in each TryReduce*/builtin reduction, not after.
From 00348ef5c3c11842465ca11e7e39706dbe91ed86 Mon Sep 17 00:00:00 2001
From: Marja Hölttä <[email protected]>
Date: Fri, 31 Oct 2025 12:34:52 +0100
Subject: [PATCH] [maglev] Fix a bug in reducing Array.p.slice

We might be in a polymorphic branch where the receiver is surely
not a JSReceiver (although we don't figure it out based on the
possible maps).

Fixed: 456547591
Change-Id: I2dc6b92ec9c537d72543b570c3e26bd1112c7f3f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7106059
Commit-Queue: Marja Hölttä <[email protected]>
Reviewed-by: Victor Gomes <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103441}
---

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 0082441..ecd22cc 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -9204,6 +9204,10 @@
     return {};
   }
 
+  if (!CheckType(receiver, NodeType::kJSReceiver)) {
+    return {};
+  }
+
   auto possible_maps = known_node_aspects().TryGetPossibleMaps(receiver);
   if (!possible_maps) {
     return {};
Loading diff…

Original Bug Report

reported by [email protected]

Maglev - CallBuiltin (input @0 = LoadHoleyFixedDoubleArrayElement) type HoleyFloat64 is not Tagged

VERSION

V8 Version: commit hash a3d368877487647fbd9d6dca8de88c3762ab2793 (ToT)

Operating System: Linux

REPRODUCTION CASE

Please run the following JS snippet using debug version of v8 with --jit-fuzzing and --turbolev flags. With these 2 flags, the crash appears sometimes. If we add the flag --turboshaft-assert-types the crash can be triggered reliably.

for (let i1 = 100; i1; --i1) {
}
function f5(a6) {
    for (let v7 = 0; v7 < 5; v7++) {
    }
    a6.slice(); 
    function f9(a10) {
        a10.forEach(f5);  
        return a10;
    }
    f9([,1.1]);
    return f5;
}
for (let v16 = 0; v16 < 25; v16++) {
    const v17 = [f5];
    try { f5(v17); } catch (e) {}
}

It will produce the following crash (in debug build) -

#
# Fatal error in ../../src/maglev/maglev-ir.cc, line 630
# Type representation error: node #95 : CallBuiltin (input @0 = LoadHoleyFixedDoubleArrayElement) type HoleyFloat64 is not Tagged
#
#
#
#FailureMessage Object: 0x7f8db1b655c8
==== C stack trace ===============================

    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7f8dbf4e76ce]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libplatform.so(+0x4ffcd) [0x7f8dbf449fcd]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x205) [0x7f8dbf4bcb85]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::maglev::CheckValueInputIs(v8::internal::maglev::NodeBase const*, int, v8::internal::maglev::ValueRepresentation)+0x207) [0x7f8dc9ba59c7]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::maglev::CallBuiltin::VerifyInputs() const+0x1ee) [0x7f8dc9ba676e]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::maglev::ProcessResult v8::internal::maglev::MaglevGraphVerifier::Process<v8::internal::maglev::CallBuiltin>(v8::internal::maglev::CallBuiltin*, v8::internal::maglev::ProcessingState const&)+0x1b5) [0x7f8dc9620185]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::maglev::GraphProcessor<v8::internal::maglev::MaglevGraphVerifier>::ProcessNodeBase(v8::internal::maglev::NodeBase*, v8::internal::maglev::ProcessingState const&)+0x11b7) [0x7f8dc9612c67]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::maglev::GraphProcessor<v8::internal::maglev::MaglevGraphVerifier>::ProcessGraph(v8::internal::maglev::Graph*)+0x446) [0x7f8dc96108a6]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::compiler::turboshaft::RunMaglevOptimizations(v8::internal::compiler::turboshaft::PipelineData*, v8::internal::maglev::MaglevCompilationInfo*, v8::internal::maglev::Graph*)+0x2f5) [0x7f8dcbae3895]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::compiler::turboshaft::TurbolevGraphBuildingPhase::Run(v8::internal::compiler::turboshaft::PipelineData*, v8::internal::Zone*, v8::internal::compiler::Linkage*)+0x3de) [0x7f8dcbb4d27e]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(auto v8::internal::compiler::turboshaft::Pipeline::Run<v8::internal::compiler::turboshaft::TurbolevGraphBuildingPhase, v8::internal::compiler::Linkage*&>(v8::internal::compiler::Linkage*&)+0x1ce) [0x7f8dcaab767e]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::compiler::turboshaft::Pipeline::CreateGraphWithMaglev(v8::internal::compiler::Linkage*)+0xa4) [0x7f8dcaaabc54]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::compiler::PipelineCompilationJob::ExecuteJobImpl(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*)+0x1ab) [0x7f8dcaa97b0b]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::OptimizedCompilationJob::ExecuteJob(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*)+0x128) [0x7f8dc7e1b028]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::OptimizingCompileTaskExecutor::RunCompilationJob(v8::internal::OptimizingCompileTaskState&, v8::internal::Isolate*, v8::internal::LocalIsolate&, v8::internal::TurbofanCompilationJob*)+0x23c) [0x7f8dc7f8411c]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8.so(v8::internal::OptimizingCompileTaskExecutor::CompileTask::Run(v8::JobDelegate*)+0x35b) [0x7f8dc7f8c49b]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libplatform.so(v8::platform::DefaultJobWorker::Run()+0xbe) [0x7f8dbf44899e]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libplatform.so(v8::platform::DefaultWorkerThreadsTaskRunner::WorkerThread::Run()+0xac) [0x7f8dbf4500fc]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libbase.so(v8::base::Thread::NotifyStartedAndRun()+0x32) [0x7f8dbf4e6dd2]
    /home/br4v3h3r0/v8/v8/out/x64.debug/libv8_libbase.so(+0x7b41a) [0x7f8dbf4e541a]
    /lib/x86_64-linux-gnu/libpthread.so.0(+0x8609) [0x7f8dbeede609]
    /lib/x86_64-linux-gnu/libc.so.6(clone+0x43) [0x7f8dbecb4353]
Trace/breakpoint trap

It is important to note that this crash is only reproducible in the debug version of V8.

Bisect

After bisect it seems that the bug was introduced in the following commit -

[maglev, turbolev] Implement array.prototype.slice -> [ebcd571a1d1] Revision: 101590

https://chromium.googlesource.com/v8/v8/+/ebcd571a1d1%5E%21/#F2

Analysis

The crash seems to occur due to a type representation mismatch in Maglev’s intermediate representation. The error message indicates that a CallBuiltin node for Builtin::kCloneFastJSArray is receiving an input with HoleyFloat64 representation when it expects Tagged representation.

During graph verification, CallBuiltin::VerifyInputs() validates that all inputs match the expected types specified by the builtin’s call interface descriptor. The CloneFastJSArray builtin expects its source parameter to be MachineType::AnyTagged(), which corresponds to ValueRepresentation::kTagged in Maglev. However, the actual input is a LoadHoleyFixedDoubleArrayElement node, which produces ValueRepresentation::kHoleyFloat64 - a special representation for loading double values from holey arrays that can contain holes.

The fundamental issue is that the optimization incorrectly passes a non-tagged value (an array element loaded from a double array) where a tagged object (the array itself) is expected. This happens because the receiver value somehow becomes confused with or replaced by an element loaded from an array during optimization passes.

Bisect Analysis

The bug was introduced in commit ebcd571a1d1c593ef03d18445a2573ec6c92c66d which implemented the TryReduceArrayPrototypeSlice optimization for Maglev.

The implementation follows a similar pattern to other array builtin optimizations like TryReduceArrayPrototypeAt, but seems to contain a critical missing validation step. While it checks that the receiver has possible maps that support fast array iteration and validates the start/end parameters, it fails to verify that the receiver itself has the correct type representation before passing it to BuildCallBuiltin.

Other similar optimization functions in the same file, such as TryReduceArrayPrototypeEntries, include an explicit check to ensure the receiver is a JSReceiver before proceeding. This check is absent from TryReduceArrayPrototypeSlice, allowing the optimization to proceed even when the receiver has an incompatible representation.

PoC Analysis

The PoC creates a scenario that triggers the bug through complex control flow involving inlining, nested function calls, and polymorphic receiver types:

  1. The outer loop calls f5([f5]) repeatedly, where the receiver is an array of functions with PACKED_ELEMENTS kind.

  2. Inside f5, it calls a6.slice() and then defines and invokes f9([,1.1]), which creates a HOLEY_DOUBLE_ELEMENTS array.

  3. The forEach call on this array invokes f5 recursively with each element: first with undefined (converted from the hole), then with the double value 1.1.

During optimization, when Maglev tries to inline these calls, it encounters multiple call sites to f5 with different receiver types. The compiler must create phi nodes to merge control flow paths where the receiver could be:

  • The original array [f5] (Tagged, PACKED_ELEMENTS)
  • Elements from the double array (potentially HoleyFloat64 for double elements)

The bug manifests when a phi node or value numbering incorrectly merges or aliases the receiver parameter with a LoadHoleyFixedDoubleArrayElement node from an inlined forEach iteration. When TryReduceArrayPrototypeSlice attempts to optimize the a6.slice() call, it receives this incorrectly-typed value node and passes it directly to BuildCallBuiltin<Builtin::kCloneFastJSArray>({receiver}) without validating its representation. The verification phase then catches this type error when CallBuiltin::VerifyInputs() discovers that input @0 has HoleyFloat64 representation instead of the required Tagged representation.

Possible Fix

The fix could add an explicit type check for the receiver at the beginning of TryReduceArrayPrototypeSlice, following the established pattern used by other array prototype optimizations in the same file.

Specifically, after obtaining the receiver value, the function should call CheckType(receiver, NodeType::kJSReceiver) and return early if the check fails.

Reporter Credit: @streypaws

View on issue tracker