CVE-2025-13630
Overview
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 aNumberorString). - `CheckType`
- a helper that queries a node’s statically known
NodeTypeand 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.
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
- Reach the slice reducer polymorphically
Craft JavaScript that calls
Array.prototype.slicein a polymorphic call site so Maglev attempts to reduce it with areceiverwhose static type includes a non-JSReceiverpossibility. - 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.
- Enter the specialized fast path
Without the guard, the reducer continues to
TryGetPossibleMapsand emits slice code that assumes aJSReceiver/array layout for a value that is not one, producing a type-confused node graph. - 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/maglev/maglev-graph-builder.cc |
modified |
Files Changed
src/maglev/maglev-graph-builder.cc
Audit Directions
- Map info used as a type proxyAudit other builtin reducers that call
TryGetPossibleMaps(or otherwise branch on possible maps) without a precedingCheckType(..., kJSReceiver); possible-map sets do not encode a proven non-JSReceivercase. - Polymorphic receiver assumptionsLook for fast paths that assume the receiver is an object in polymorphic branches, especially array/
this-based builtins reduced in Maglev. - Reducer early-exit orderingVerify that type-establishing guards run before any map- or shape-dependent specialization in each
TryReduce*/builtin reduction, not after.
Patch
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 {};
Original Bug Report
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:
-
The outer loop calls
f5([f5])repeatedly, where the receiver is an array of functions withPACKED_ELEMENTSkind. -
Inside
f5, it callsa6.slice()and then defines and invokesf9([,1.1]), which creates aHOLEY_DOUBLE_ELEMENTSarray. -
The
forEachcall on this array invokesf5recursively 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
HoleyFloat64for 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