CVE-2026-7927
Overview
Background
- `Array.prototype.slice` fast path
- V8 has Torque (
.tq) builtins that slice arguments objects without the general (slow) runtime path when the result is small enough to fit in new space. - New-space allocation folding
- V8 can “fold” the
JSArrayobject and its backingFixedArrayelements into a single contiguous young-generation allocation, which requires the combined size to fit within the new-space fixed-array limit. - `kMaxNewSpaceFixedArrayElements`
- the maximum element count a
FixedArraymay have while still being allocatable in new space. - `kJSArrayHeaderSizeSlots`
- a constant added by this patch equal to
JSArray::kHeaderSize / kTaggedSize, i.e. the number of tagged slots theJSArrayheader occupies.
Root Cause Analysis
The arguments-slice fast-path macros bailed to the slow path only when count >= kMaxNewSpaceFixedArrayElements, i.e. they accepted any count strictly below the bare FixedArray element limit. But the fast path then folds the JSArray header and the FixedArray of count elements into one new-space allocation, so the true space consumed is count elements plus the header slots. For a count just under kMaxNewSpaceFixedArrayElements, the folded allocation count + kJSArrayHeaderSizeSlots exceeds the new-space fixed-array capacity, so the threshold check was off by the header size and permitted an allocation larger than the invariant allows — producing a malformed/overlapping object layout that is observable as type confusion rather than a clean bailout.
The fix tightens the bailout condition to count >= kMaxNewSpaceFixedArrayElements - kJSArrayHeaderSizeSlots, reserving room for the header so the folded JSArray + elements allocation is always valid; oversized slices now take the Bailout slow path instead.
JSArray header too, so subtracting kJSArrayHeaderSizeSlots from the limit is what restores the invariant that the folded object fits in new space.Attack Path
- Craft a large arguments object
Script creates a function call whose
arguments(mapped/sloppy or simple) has a length chosen so a slice yields acountjust belowkMaxNewSpaceFixedArrayElements. - Invoke the fast slice
Calling
Array.prototype.sliceon that arguments object reachesHandleSimpleArgumentsSlice(or the sloppy-arguments variant) with thatcount, which passes the old, too-loose threshold instead of bailing out. - Fold an oversized allocation
The fast path folds the
JSArrayheader pluscountelements into a single new-space allocation whose total exceeds the fixed-array limit, violating the folding invariant. - Trigger type confusion The resulting mis-sized/overlapping object layout is interpreted inconsistently by the engine, the type confusion the advisory describes (any further exploitation primitive is inferred and not demonstrated by the diff).
Impact Assessment
Array.prototype.slice on an arguments object, requiring no special permissions. The advisory rates this high severity and classifies it as type confusion in Runtime (V8); it does not state in-the-wild exploitation. Any escalation from the type confusion to a memory read/write primitive or code execution is (inferred) and not shown by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/builtins/array-slice.tq |
modified |
Files Changed
src/builtins/array-slice.tq
Audit Directions
- Folded-allocation size mathAudit every fast path that folds a header object with a variable-length backing store into one allocation; confirm the capacity check accounts for header slots, not just element count.
- Off-by-header thresholdsReview Torque builtins that compare a user-influenced
countagainstkMaxNewSpace*/capacity limits for strict-vs-inclusive and header-inclusive correctness before bailing. - Arguments-object fast pathsCheck other arguments/array builtins (
concat,splice, spread) that special-caseJSArgumentsObjectWithLength/SloppyArgumentsElementsfor the same unbounded fast-path entry.
Patch
From 615d074ab27797662bd4c4e1fc5236a1fccce022 Mon Sep 17 00:00:00 2001 From: Marja Hölttä <[email protected]> Date: Thu, 16 Apr 2026 14:56:51 +0200 Subject: [PATCH] [builtins] Fix Array.p.slice bailout threshold on arguments objects Fixed: 502830119 Change-Id: Ia5b299cd9c33cabfe2a0647ff6a54e1942d6cd3f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7766691 Commit-Queue: Marja Hölttä <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Cr-Commit-Position: refs/heads/main@{#106550} --- diff --git a/src/builtins/array-slice.tq b/src/builtins/array-slice.tq index bb56285..d3f80e5 100644 --- a/src/builtins/array-slice.tq +++ b/src/builtins/array-slice.tq @@ -3,12 +3,21 @@ // found in the LICENSE file. namespace array { + +const kJSArrayHeaderSizeSlots: + constexpr int31 generates 'JSArray::kHeaderSize / kTaggedSize'; + macro HandleSimpleArgumentsSlice( context: NativeContext, args: JSArgumentsObjectWithLength, start: Smi, count: Smi): JSArray labels Bailout { // If the resulting array doesn't fit in new space, use the slow path. - if (count >= kMaxNewSpaceFixedArrayElements) goto Bailout; + // We subtract the JSArray header size to ensure the JSArray and elements + // allocation can be safely folded together into a single object. + if (count >= kMaxNewSpaceFixedArrayElements - kJSArrayHeaderSizeSlots) { + goto Bailout; + } + const end: Smi = start + count; const sourceElements: FixedArray = @@ -32,7 +41,11 @@ count: Smi): JSArray labels Bailout { // If the resulting array doesn't fit in new space, use the slow path. - if (count >= kMaxNewSpaceFixedArrayElements) goto Bailout; + // We subtract the JSArray header size to ensure the JSArray and elements + // allocation can be safely folded together into a single object. + if (count >= kMaxNewSpaceFixedArrayElements - kJSArrayHeaderSizeSlots) { + goto Bailout; + } const sloppyElements: SloppyArgumentsElements = Cast<SloppyArgumentsElements>(args.elements) otherwise Bailout;
Original Bug Report
Missing write barrier in Array slice for sloppy arguments due to allocation folding threshold mismatch
Project Fortify has identified a security issue and generated a PoC.
d8 variant: ‘Default’
flags: –allow-natives-syntax –expose-gc –no-inline-new –scavenger-chaos-mode –scavenger-chaos-mode-threshold=90 –verify-heap –omit-quit –fuzzing
Return code: 134
<details>
<summary>stdout</summary>
Starting test...
Attempt 0
</details>
<details>
<summary>stderr</summary>
#
# Fatal error in ../../v8/src/heap/heap-verifier.cc, line 566
# Check failed: untyped_->count(slot.address()) > 0 (0 vs. 0).
#
#
#
#FailureMessage Object: 0x7ffe3ae8a9e0
==== C stack trace ===============================
/compressed/bin/Default/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7fb2375f489e]
/compressed/bin/Default/libv8_libplatform.so(+0x163cd) [0x7fb23d4d43cd]
/compressed/bin/Default/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x194) [0x7fb2375d6b14]
/compressed/bin/Default/libv8.so(+0x26fb1e5) [0x7fb239cfb1e5]
/compressed/bin/Default/libv8.so(+0x2729a96) [0x7fb239d29a96]
/compressed/bin/Default/libv8.so(+0x26f86f4) [0x7fb239cf86f4]
/compressed/bin/Default/libv8.so(v8::internal::PagedSpaceBase::Verify(v8::internal::Isolate*, v8::internal::SpaceVerificationVisitor*) const+0x156) [0x7fb239ec3176]
/compressed/bin/Default/libv8.so(+0x26f7412) [0x7fb239cf7412]
/compressed/bin/Default/libv8.so(v8::internal::HeapVerifier::VerifyHeap(v8::internal::Heap*)+0x87) [0x7fb239cf8e77]
/compressed/bin/Default/libv8.so(+0x276efbd) [0x7fb239d6efbd]
/compressed/bin/Default/libv8.so(+0x276df86) [0x7fb239d6df86]
/compressed/bin/Default/libv8.so(+0x27ad1a2) [0x7fb239dad1a2]
/compressed/bin/Default/libv8.so(+0x27acd0f) [0x7fb239dacd0f]
/compressed/bin/Default/libv8.so(+0x3da4d4b) [0x7fb23b3a4d4b]
/compressed/bin/Default/libv8.so(v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags, v8::internal::PerformHeapLimitCheck, v8::internal::PerformIneffectiveMarkCompactCheck)+0x473) [0x7fb239d69b93]
/compressed/bin/Default/libv8.so(+0x2579c9e) [0x7fb239b79c9e]
/compressed/bin/Default/libv8.so(+0x25797f4) [0x7fb239b797f4]
/compressed/bin/Default/libv8.so(+0xf94930) [0x7fb238594930]
Received signal 6
Aborted (core dumped)
</details>
Overview: A size threshold mismatch between Torque and the CodeStubAssembler allows certain JSArray allocations to be unfolded despite passing safety checks. This unfolded allocation can trigger a garbage collection that promotes the array elements to old space, while elements are copied into it without a write barrier. This results in stale tagged pointers and V8 heap type confusion.
Affected files:
v8/src/builtins/array-slice.tqv8/src/codegen/code-stub-assembler.cc
Estimated timestamp from git blame: 2025-03-31
Root Cause
In v8/src/builtins/array-slice.tq, the HandleFastAliasedSloppyArgumentsSlice macro attempts to optimize Array.prototype.slice on fast-aliased sloppy arguments. It guards the fast path by checking count >= kMaxNewSpaceFixedArrayElements (which resolves to FixedArray::kMaxRegularLength, typically 32766 on pointer-compressed builds). If the check passes, it allocates a new JSArray and copies elements into its backing store using UNSAFE_SKIP_WRITE_BARRIER.
The use of UNSAFE_SKIP_WRITE_BARRIER relies on the assumption that the JSArray header and its FixedArray elements will be allocated as a single contiguous “folded” block in new-space, guaranteeing the elements array cannot be in old-space.
However, in CodeStubAssembler::AllocateUninitializedJSArrayWithElements, the decision to fold the allocation is based on whether the combined size of the JSArray header and the FixedArray elements is within kMaxRegularHeapObjectSize (128 KB). For a count value of 32763, the FixedArray alone fits within kMaxRegularHeapObjectSize (so it passes the Torque guard), but the combined size including the 16-byte JSArray header exceeds 131072 bytes.
Because the combined size is too large, the CSA performs an unfolded allocation: it allocates the FixedArray elements first, and then allocates the JSArray header separately. If the new-space is near capacity, the second allocation will trigger a scavenge garbage collection. During this scavenge, the just-allocated FixedArray can be promoted to the old generation.
When execution returns to Torque, it writes young-generation mapped parameters into this now old-generation FixedArray using UNSAFE_SKIP_WRITE_BARRIER. This old-to-new pointer is never recorded in the remembered set. Upon the next minor GC, the young objects are collected, leaving dangling pointers inside the elements array and leading to highly exploitable V8 heap type confusion.
Suggested Fix
There are two potential fixes for this issue:
- Use standard write barriers: The most robust fix is to remove
UNSAFE_SKIP_WRITE_BARRIERand use the defaultStoreFixedArrayElementwhich emits proper write barriers. The performance overhead in this specific slice path is likely negligible. - Adjust the bailout threshold: If the barrier-skip optimization is strictly required, the bailout threshold in
HandleFastAliasedSloppyArgumentsSlicemust be tightened to account for theJSArrayheader size (e.g., subtractingJSArray::kHeaderSize / kTaggedSizefrom the maximum capacity check) to guarantee the allocation will be safely folded.
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
The description of the vuln is LLM-generated and can contain mistakes. Your feedback is appreciated, and will help us make improvement over time. The PoC was run in a VM and it seemed to be legit - if not, let us know and we can strengthen our checker.