CVE-2026-87564
Overview
Background
- `Array.prototype.flat`
- a JavaScript builtin that produces a new array with nested sub-array elements recursively concatenated up to a specified depth.
- Torque (`.tq`)
- V8’s typed domain-specific language for implementing builtins, where type casts like
Cast<T>are checked andUnsafeCast<T>are not. - `Smi`
- a “small integer,” V8’s tagged-pointer representation of an integer that fits inline in a machine word without a heap allocation, as opposed to a
HeapNumber. - `FastJSArray`
- an array whose backing store uses a fast, packed elements kind, generally assumed to carry a
Smilength.
Root Cause Analysis
The fast-path loop in src/builtins/array-flat.tq read the source array’s length into sourceLength and then narrowed it to a Smi via UnsafeCast<Smi>(sourceLength), guarded only by a dcheck(Is<Smi>(sourceLength)). The code justified this with the comment that a FastJSArray “length must be a Smi,” but a dcheck is compiled out of release builds, so nothing at runtime enforced the invariant. If sourceLength was actually a HeapNumber rather than a Smi, UnsafeCast<Smi> reinterpreted the boxed-double pointer as a tagged small integer, yielding a corrupted smiSourceLength used as the loop bound over fastSource.
The fix replaces the unchecked narrowing with Cast<Smi>(sourceLength) otherwise goto Bailout(...), so a non-Smi length now safely diverts to the generic slow path instead of being misinterpreted.
dcheck invariant to make an UnsafeCast<Smi> sound at runtime; the fix replaces it with a real Cast<Smi> whose failure branch bails to the safe slow path.Attack Path
- Shape an array with a non-`Smi` length
Arrange for a
FastJSArrayreaching theflatfast path to hold its length as aHeapNumberrather than aSmi, violating the assumed invariant. - Invoke `Array.prototype.flat`
Call
flatso execution enters the fast-path loop inarray-flat.tqand reaches theUnsafeCast<Smi>(sourceLength). - Trigger the misinterpretation
In a release build the
dcheckis absent, so theHeapNumberpointer is reinterpreted as aSmi, producing a bogussmiSourceLengthbound. - Drive confused iteration
The corrupted
smiSourceLengthcontrols thefor (; smiSourceIndex < smiSourceLength; ...)loop overfastSource, mixing a type-confused value into array traversal.
Impact Assessment
Smi and a HeapNumber used as an array-length loop bound, which can lead to out-of-bounds behavior over the fast array’s elements. Exploitation requires only that untrusted script reach the Array.prototype.flat fast path with a source array whose length is not a Smi, and takes effect only in release builds where the dcheck guard is compiled out.Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/builtins/array-flat.tq |
modified |
Files Changed
src/builtins/array-flat.tq
Audit Directions
- `UnsafeCast` justified by `dcheck`Flag any
UnsafeCast<T>in Torque whose safety rests on an adjacentdcheck/Is<T>assumption, since that check vanishes in release builds; prefer a checkedCast<T> otherwise gotobailout. - Assumed `Smi` invariants on array lengthsAudit fast-path builtins that treat a
FastJSArrayor similar length as guaranteedSmiwithout a runtime narrowing check. - Loop bounds from unchecked castsReview loops whose iteration bound is produced by an unchecked type narrowing, as a confused bound can drive out-of-bounds element access.
Patch
From 50a6c0c69c1384720d9c3599dadfa73e55b865d7 Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
Date: Wed, 02 Sep 2026 13:14:21 +0200
Subject: [PATCH] [builtins] Fix Array.prototype.flat
This CL replaces the unsafe cast with a safe Cast<Smi>, branching to
the slow path when sourceLength is not a Smi.
TAG=agy
CONV=1183a007-e898-453b-9823-1c970699f025
Fixed: 552342545
Change-Id: I539dd7006dfece6c247565a0940aeac9d7cc669a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8346487
Auto-Submit: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Patrick Thier <pthier@chromium.org>
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109636}
---
diff --git a/src/builtins/array-flat.tq b/src/builtins/array-flat.tq
index a40b8a0..659de78 100644
--- a/src/builtins/array-flat.tq
+++ b/src/builtins/array-flat.tq
@@ -420,9 +420,8 @@
otherwise goto Bailout(targetIndex, smiSourceIndex);
let fastOW = NewFastJSArrayWitness(fastSource);
- // The source is a FastJSArray, thus its length must be a Smi.
- dcheck(Is<Smi>(sourceLength));
- const smiSourceLength = UnsafeCast<Smi>(sourceLength);
+ const smiSourceLength = Cast<Smi>(sourceLength)
+ otherwise goto Bailout(targetIndex, smiSourceIndex);
// 3. Repeat, while sourceIndex < sourceLen
for (; smiSourceIndex < smiSourceLength; smiSourceIndex++) {