CVE-2026-91728
Overview
Background
- `FeedbackVector`
- a V8 heap object holding the inline-cache and profiling feedback slots for a function, laid out as a fixed header followed by a variable-length tail of tagged slots.
- `FeedbackVector::SizeFor`
- a
constexprhelper that computes a vector’s total byte size from its slotlengthasOFFSET_OF_DATA_START(FeedbackVector) + length * kTaggedSize. - `kTaggedSize`
- the byte width of a single tagged pointer slot, the multiplier applied to
lengthwhen sizing the tail. - Integer overflow
- an arithmetic result that exceeds the range of its signed 32-bit type, wrapping to a small or negative value and thereby understating the memory a caller should allocate.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Diff adds bounds CHECKs at SizeFor only; it does not show how an oversized length arises or any undersized-allocation caller. FeedbackVectorVerify is a heap verifier, not an allocation path. Calling the wrap 'exploitable' and the OOB read/write are stated as fact without diff support.
FeedbackVector::SizeFor in src/objects/feedback-vector.h computed the object size as OFFSET_OF_DATA_START(FeedbackVector) + length * kTaggedSize using signed 32-bit int arithmetic with no bound on length. Because the function never checked that length was non-negative or within a representable maximum, a sufficiently large length made length * kTaggedSize overflow, so SizeFor returned a truncated (or negative) size that no longer described the actual number of slots. Callers using that undersized value to allocate the vector would then read and write the variable-length tail — as FeedbackVectorVerify does over raw_feedback_slots()[i] for i < len — past the end of the allocation.
The fix defines FeedbackVector::kMaxLength as (kMaxInt - kHeaderSize) / kTaggedSize, the largest length for which the size expression cannot overflow, and adds CHECK_GE(length, 0) and CHECK_LE(length, kMaxLength) to SizeFor, plus a matching CHECK_LE(len, kMaxLength) in FeedbackVectorVerify. These checks turn an exploitable wrap into a safe abort before any undersized allocation can occur.
length * kTaggedSize in bounded signed int arithmetic without ever validating length, allowing overflow to produce a size smaller than the data it must hold; the fix bounds length to kMaxLength and asserts it in SizeFor so the product can never wrap.Attack Path
- Reach vector allocation with attacker-influenced length
Drive script execution that causes V8 to create or grow a
FeedbackVectorwhose slotlengthis derived from an attacker-controllable count. - Push length past the overflow threshold
Supply a
lengthlarge enough thatlength * kTaggedSizeexceedskMaxInt, soFeedbackVector::SizeForreturns a wrapped, undersized (possibly negative) byte size. - Trigger an undersized allocation
The truncated
SizeForresult is used to allocate aFeedbackVectorfar smaller than the declaredlengthimplies. - Access the oversized tail
Slot iteration over
raw_feedback_slots()[i]for indices up to the declaredlengthreads and writes beyond the small allocation, corrupting adjacent heap objects. - Escalate the corruption Groom the heap so the out-of-bounds slots overlap sensitive objects, converting the overflow into a controlled read/write primitive within the renderer.
Impact Assessment
FeedbackVector allocation, a primitive typically escalated toward memory corruption and renderer-process compromise. The precondition is the ability to steer a FeedbackVector length close to or past the overflow boundary; the multiplication’s wrap does the rest with no further checks in the vulnerable code. Exploitation is contained to the sandboxed renderer absent a further sandbox escape.Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/diagnostics/objects-debug.cc |
modified | |
TEST_Ftest/unittests/objects/feedback-vector-unittest.cc |
modified |
Files Changed
src/diagnostics/objects-debug.cctest/unittests/objects/feedback-vector-unittest.ccsrc/objects/feedback-vector.h
Audit Directions
- Unbounded size arithmeticAudit other
SizeFor/OffsetOfElementAthelpers that computeheader + length * elementSizein signedintwithout akMaxLengthbound, since any of them can wrap on an attacker-influenced length. - Length checks split from allocationLook for object types where the length is validated at allocation but not inside the shared size helper (or vice versa), leaving a path that reaches the multiplication with an unchecked count.
- Verifier gapsCompare each object’s
Verifyroutine against its length invariants; a missingCHECK_LE(len, kMaxLength)like the one added here indicates a class whose tail can be iterated past its true bounds.
Patch
From 714b5478d3c4ff055570eebe0ee9194e518ad21e Mon Sep 17 00:00:00 2001 From: Leszek Swirski <[email protected]> Date: Mon, 07 Sep 2026 14:19:53 +0200 Subject: [PATCH] [feedback-vector] Check length in FeedbackVector::SizeFor Add FeedbackVector::kMaxLength and CHECK that the vector length is non-negative and does not exceed kMaxLength in SizeFor(). Also add a length check in FeedbackVectorVerify(). TAG=agy CONV=5e1a1d9b-ed48-4b56-91fb-9d3ead86c7d2 Fixed: 556715288 Change-Id: Ib88573d9bef7c364663538aa3801a5ded7eef182 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8366705 Reviewed-by: Patrick Thier <[email protected]> Commit-Queue: Patrick Thier <[email protected]> Auto-Submit: Leszek Swirski <[email protected]> Cr-Commit-Position: refs/heads/main@{#109703} --- diff --git a/src/diagnostics/objects-debug.cc b/src/diagnostics/objects-debug.cc index 9ef6e1b..a28dd8c 100644 --- a/src/diagnostics/objects-debug.cc +++ b/src/diagnostics/objects-debug.cc @@ -1019,6 +1019,7 @@ CHECK(IsFeedbackCell(parent_feedback_cell())); // Variable-length maybe-weak tail. const uint32_t len = length().value(); + CHECK_LE(len, static_cast<uint32_t>(kMaxLength)); for (uint32_t i = 0; i < len; ++i) { Tagged<MaybeObject> value = raw_feedback_slots()[i].Relaxed_Load(); Object::VerifyMaybeObjectPointer(isolate, value); diff --git a/src/objects/feedback-vector.h b/src/objects/feedback-vector.h index 1f2971d..4f67694 100644 --- a/src/objects/feedback-vector.h +++ b/src/objects/feedback-vector.h @@ -513,6 +513,7 @@ static const int kHeaderSize; static const int kRawFeedbackSlotsOffset; + static const int kMaxLength; static constexpr int SizeFor(int length); static constexpr int OffsetOfElementAt(int index); @@ -565,8 +566,12 @@ OFFSET_OF_DATA_START(FeedbackVector); inline constexpr int FeedbackVector::kRawFeedbackSlotsOffset = OFFSET_OF_DATA_START(FeedbackVector); +inline constexpr int FeedbackVector::kMaxLength = + (kMaxInt - kHeaderSize) / kTaggedSize; constexpr int FeedbackVector::SizeFor(int length) { + CHECK_GE(length, 0); + CHECK_LE(length, kMaxLength); return OFFSET_OF_DATA_START(FeedbackVector) + length * kTaggedSize; } constexpr int FeedbackVector::OffsetOfElementAt(int index) { diff --git a/test/unittests/objects/feedback-vector-unittest.cc b/test/unittests/objects/feedback-vector-unittest.cc index a5acfac..84081da 100644 --- a/test/unittests/objects/feedback-vector-unittest.cc +++ b/test/unittests/objects/feedback-vector-unittest.cc @@ -783,5 +783,26 @@ CHECK_EQ(InlineCacheState::MONOMORPHIC, nexus.ic_state()); } +TEST_F(FeedbackVectorTest, MaxLengthAndSizeFor) { + static_assert(FeedbackVector::SizeFor(0) == FeedbackVector::kHeaderSize); + static_assert(FeedbackVector::SizeFor(10) == + FeedbackVector::kHeaderSize + 10 * kTaggedSize); + static_assert(FeedbackVector::SizeFor(FeedbackVector::kMaxLength) == + FeedbackVector::kHeaderSize + + FeedbackVector::kMaxLength * kTaggedSize); + + EXPECT_EQ(FeedbackVector::kHeaderSize, FeedbackVector::SizeFor(0)); + EXPECT_EQ(FeedbackVector::kHeaderSize + 10 * kTaggedSize, + FeedbackVector::SizeFor(10)); + EXPECT_EQ( + FeedbackVector::kHeaderSize + FeedbackVector::kMaxLength * kTaggedSize, + FeedbackVector::SizeFor(FeedbackVector::kMaxLength)); + + ASSERT_DEATH_IF_SUPPORTED({ FeedbackVector::SizeFor(-1); }, ".*"); + ASSERT_DEATH_IF_SUPPORTED( + { FeedbackVector::SizeFor(FeedbackVector::kMaxLength + 1); }, ".*"); + ASSERT_DEATH_IF_SUPPORTED({ FeedbackVector::SizeFor(1 << 30); }, ".*"); +} + } // namespace internal } // namespace v8
Regression Test / PoC
diff --git a/test/unittests/objects/feedback-vector-unittest.cc b/test/unittests/objects/feedback-vector-unittest.cc
index a5acfac..84081da 100644
--- a/test/unittests/objects/feedback-vector-unittest.cc
+++ b/test/unittests/objects/feedback-vector-unittest.cc
@@ -783,5 +783,26 @@
CHECK_EQ(InlineCacheState::MONOMORPHIC, nexus.ic_state());
}
+TEST_F(FeedbackVectorTest, MaxLengthAndSizeFor) {
+ static_assert(FeedbackVector::SizeFor(0) == FeedbackVector::kHeaderSize);
+ static_assert(FeedbackVector::SizeFor(10) ==
+ FeedbackVector::kHeaderSize + 10 * kTaggedSize);
+ static_assert(FeedbackVector::SizeFor(FeedbackVector::kMaxLength) ==
+ FeedbackVector::kHeaderSize +
+ FeedbackVector::kMaxLength * kTaggedSize);
+
+ EXPECT_EQ(FeedbackVector::kHeaderSize, FeedbackVector::SizeFor(0));
+ EXPECT_EQ(FeedbackVector::kHeaderSize + 10 * kTaggedSize,
+ FeedbackVector::SizeFor(10));
+ EXPECT_EQ(
+ FeedbackVector::kHeaderSize + FeedbackVector::kMaxLength * kTaggedSize,
+ FeedbackVector::SizeFor(FeedbackVector::kMaxLength));
+
+ ASSERT_DEATH_IF_SUPPORTED({ FeedbackVector::SizeFor(-1); }, ".*");
+ ASSERT_DEATH_IF_SUPPORTED(
+ { FeedbackVector::SizeFor(FeedbackVector::kMaxLength + 1); }, ".*");
+ ASSERT_DEATH_IF_SUPPORTED({ FeedbackVector::SizeFor(1 << 30); }, ".*");
+}
+
} // namespace internal
} // namespace v8