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 <[email protected]> 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 <[email protected]> Reviewed-by: Patrick Thier <[email protected]> Commit-Queue: Patrick Thier <[email protected]> Commit-Queue: Igor Sheludko <[email protected]> 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++) {