High chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Bug ClassInteger Overflow
Tracker511218177
Fix commitc055ccbcde8e (v8/v8) +28/-24
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/heap/cppgc/heap-page.cc
  • src/heap/cppgc/heap-page.h
  • src/heap/cppgc/object-start-bitmap.h
From c055ccbcde8ea8c6f5356fd9f5fb3d6a6b73a9ee Mon Sep 17 00:00:00 2001
From: Anton Bikineev <bikineev@chromium.org>
Date: Tue, 12 May 2026 14:02:17 +0200
Subject: [PATCH] [cppgc] Fix memory ordering race in ObjectStartBitmap

A memory ordering race on weak memory architectures (like ARM64) between
mutator allocations and the concurrent marker could lead to an integer
underflow in ObjectStartBitmap::FindHeader. This happened because the
concurrent marker could observe a new pointer in a cppgc::Member before
the corresponding bit was set in the ObjectStartBitmap.

The CL fixes it by using a seq-cst OSB write. As a drive-by, it fixes
the issue with conservative object lookup.

Bug: 511218177
Change-Id: I89e8fa26966daf755380719277be0a1656268940
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7840600
Auto-Submit: Anton Bikineev <bikineev@chromium.org>
Commit-Queue: Michael Lippautz <mlippautz@chromium.org>
Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107284}
---

diff --git a/src/heap/cppgc/heap-page.cc b/src/heap/cppgc/heap-page.cc
index 7202b72..5b1b806 100644
--- a/src/heap/cppgc/heap-page.cc
+++ b/src/heap/cppgc/heap-page.cc
@@ -108,6 +108,7 @@
             static_cast<ConstAddress>(address))) {
       return nullptr;
     }
+    return LargePage::From(this)->ObjectHeader();
   } else {
     const NormalPage* normal_page = NormalPage::From(this);
     if (!normal_page->PayloadContains(static_cast<ConstAddress>(address))) {
@@ -117,14 +118,20 @@
     DCHECK(!NormalPageSpace::From(normal_page->space())
                 .linear_allocation_buffer()
                 .size());
-  }
 
-  // |address| is on the heap, so we FromInnerAddress can get the header.
-  const HeapObjectHeader* header =
-      ObjectHeaderFromInnerAddressImpl(this, address);
-  if (header->IsFree()) return nullptr;
-  DCHECK_NE(kFreeListGCInfoIndex, header->GetGCInfoIndex());
-  return header;
+    const PlatformAwareObjectStartBitmap& bitmap =
+        NormalPage::From(this)->object_start_bitmap();
+    const HeapObjectHeader* header = bitmap.FindHeader<AccessMode::kNonAtomic>(
+        static_cast<ConstAddress>(address));
+    if (address >= reinterpret_cast<ConstAddress>(header) +
+                       header->AllocatedSize<AccessMode::kAtomic>()) {
+      // Found an object below the current address.
+      return nullptr;
+    }
+    if (header->IsFree()) return nullptr;
+    DCHECK_NE(kFreeListGCInfoIndex, header->GetGCInfoIndex());
+    return header;
+  }
 }
 
 #if defined(CPPGC_YOUNG_GENERATION)
diff --git a/src/heap/cppgc/heap-page.h b/src/heap/cppgc/heap-page.h
index 313f2f5..cf3afb2 100644
--- a/src/heap/cppgc/heap-page.h
+++ b/src/heap/cppgc/heap-page.h
@@ -321,21 +321,6 @@
   return static_cast<const BasePage*>(BasePageHandle::FromPayload(payload));
 }
 
-template <AccessMode mode = AccessMode::kNonAtomic>
-const HeapObjectHeader* ObjectHeaderFromInnerAddressImpl(const BasePage* page,
-                                                         const void* address) {
-  if (page->is_large()) {
-    return LargePage::From(page)->ObjectHeader();
-  }
-  const PlatformAwareObjectStartBitmap& bitmap =
-      NormalPage::From(page)->object_start_bitmap();
-  const HeapObjectHeader* header =
-      bitmap.FindHeader<mode>(static_cast<ConstAddress>(address));
-  DCHECK_LT(address, reinterpret_cast<ConstAddress>(header) +
-                         header->AllocatedSize<AccessMode::kAtomic>());
-  return header;
-}
-
 template <AccessMode mode>
 HeapObjectHeader& BasePage::ObjectHeaderFromInnerAddress(void* address) const {
   return const_cast<HeapObjectHeader&>(
@@ -352,8 +337,15 @@
   // the page |type_| field). This can occur when tracing a Member holding a
   // reference to a mixin type
   SynchronizedLoad();
+  if (is_large()) {
+    return *LargePage::From(this)->ObjectHeader();
+  }
+  const PlatformAwareObjectStartBitmap& bitmap =
+      NormalPage::From(this)->object_start_bitmap();
   const HeapObjectHeader* header =
-      ObjectHeaderFromInnerAddressImpl<mode>(this, address);
+      bitmap.FindHeader<mode>(static_cast<ConstAddress>(address));
+  DCHECK_LT(address, reinterpret_cast<ConstAddress>(header) +
+                         header->AllocatedSize<AccessMode::kAtomic>());
   DCHECK_NE(kFreeListGCInfoIndex, header->GetGCInfoIndex<mode>());
   return *header;
 }
diff --git a/src/heap/cppgc/object-start-bitmap.h b/src/heap/cppgc/object-start-bitmap.h
index 112ed65..15b9de4 100644
--- a/src/heap/cppgc/object-start-bitmap.h
+++ b/src/heap/cppgc/object-start-bitmap.h
@@ -129,6 +129,8 @@
     DCHECK_LT(0u, cell_index);
     byte = load<mode>(--cell_index);
   }
+  // Crash safely instead of returning page_base - 8.
+  CHECK_NE(0, byte);
   const int leading_zeroes = v8::base::bits::CountLeadingZeros(byte);
   object_start_number =
       (cell_index * kBitsPerCell) + (kBitsPerCell - 1) - leading_zeroes;
@@ -166,8 +168,11 @@
     object_start_bit_map_[cell_index] = value;
     return;
   }
+  // Use seq cst here to avoid a situation when a pointer write may be reordered
+  // before the setting of the mark bit, which may lead to the concurrent marker
+  // missing the right object (because the bit is not yet propagated).
   std::atomic_ref<uint8_t>(object_start_bit_map_[cell_index])
-      .store(value, std::memory_order_release);
+      .store(value, std::memory_order_seq_cst);
 }
 
 template <AccessMode mode>
Loading diff…

Original Bug Report

reported by vm...@google.com

cppgc: Memory ordering race in ObjectStartBitmap allows RCE via underflow

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A memory ordering race on weak memory architectures (like ARM64) between mutator allocations and the concurrent marker leads to an integer underflow in ObjectStartBitmap::FindHeader. This returns an out-of-bounds pointer into an adjacent heap page, causing the marker to trace forged headers and internal page structures, leading to arbitrary code execution.

Affected files:

  • v8/src/heap/cppgc/object-start-bitmap.h
  • v8/src/heap/cppgc/member-storage.h
  • v8/src/heap/cppgc/concurrent-marker.cc
  • v8/src/heap/cppgc/heap-page.h
  • v8/src/heap/cppgc/trace-trait.cc
  • v8/src/heap/cppgc/marking-state.h

Estimated timestamp from git blame: 2023-02-27

Summary

A vulnerability exists in V8’s cppgc (Oilpan) garbage collector. On weak memory architectures like ARM64, a memory ordering race can occur when assigning a newly allocated object to a cppgc::Member. The concurrent marker can observe the new pointer before the corresponding bit is set in the ObjectStartBitmap. This stale state triggers an integer underflow in ObjectStartBitmap::FindHeader, resulting in an out-of-bounds pointer. Because cppgc pages lack guard pages, this pointer resolves to the payload of the adjacent preceding page, allowing an attacker to forge a HeapObjectHeader, force type confusion on internal heap metadata, and achieve arbitrary code execution in the renderer process.

Technical Details

1. The Memory Ordering Race

When a mutator thread allocates a new object on a fresh page, it registers the object’s start address in the page’s bitmap. This is done via ObjectStartBitmap::SetBit<AccessMode::kAtomic>, which performs a std::memory_order_release store.

Subsequently, if this new pointer is assigned to a cppgc::Member of an object currently being traced, the pointer is written using std::memory_order_relaxed (v8/include/cppgc/internal/member-storage.h:102). The assignment triggers a write barrier, but the fast path only performs relaxed checks and does not emit a strong memory fence (like memory_order_seq_cst).

On ARM64, a Store-Release prevents prior operations from being reordered after it, but it does not prevent subsequent operations from being reordered before it. Consequently, the relaxed store of the Member pointer can become globally visible before the release store to the bitmap.

2. The Integer Underflow

The concurrent marker background thread, reading the Member via an acquire/relaxed load, observes the new pointer but not the updated bitmap. To trace the object (specifically a mixin), it calls ObjectStartBitmap::FindHeader to locate the HeapObjectHeader.

FindHeader scans the bitmap backwards to find the nearest set bit. Because the page is fresh and the mutator’s bit isn’t visible, every cell is 0. The loop (while (!byte && cell_index) { byte = load<mode>(--cell_index); }) decrements cell_index until it reaches 0.

The function then executes:

const int leading_zeroes = v8::base::bits::CountLeadingZeros(byte);
object_start_number = (cell_index * kBitsPerCell) + (kBitsPerCell - 1) - leading_zeroes;

Passing 0 to CountLeadingZeros returns 8 (v8/src/base/bits.h). Substituting the values yields (0 * 8) + 7 - 8 = -1. Because object_start_number is a size_t, -1 underflows to SIZE_MAX.

The code then calculates object_offset = object_start_number * kAllocationGranularity (where granularity is 8). Due to two’s complement arithmetic, SIZE_MAX * 8 results in -8. FindHeader returns page_base - 8.

3. Heap Adjacency and Exploitation

In cppgc, NormalPage allocations are 128KB contiguous blocks within the Caged Heap, with no implicit guard pages between them (v8/src/heap/cppgc/page-memory.cc). The payload of a NormalPage can extend precisely to the 128KB boundary. Therefore, page_base - 8 points to the last 8 bytes of the payload of the immediately preceding page (Page A).

An attacker can reliably trigger this condition by:

  1. Grooming the heap to control the last 8 bytes of Page A.
  2. Forging a HeapObjectHeader in those 8 bytes, setting the 14-bit GCInfoIndex (offset +4 on 64-bit) to correspond to a useful Trace callback, and leaving the mark bit (offset +6) as 0.
  3. Allocating a new mixin object on Page B (triggering the race).

When FindHeader returns page_base - 8, the marker casts it to a HeapObjectHeader and reads the attacker’s forged GCInfoIndex. It then performs an atomic CAS to flip the mark bit at page_base - 2, which succeeds.

Finally, the marker calculates the payload pointer by adding sizeof(HeapObjectHeader) (8 bytes) to the returned pointer: (page_base - 8) + 8 = page_base. It then invokes the attacker’s chosen Trace function on this pointer.

Because page_base is exactly offset 0 of Page B, it points to the NormalPage struct itself. The Trace function interprets the internal V8 heap metadata (such as Space*, SlotSet*, and the massive ObjectStartBitmap array) as user-defined pointers. This massive type confusion allows the attacker to corrupt Oilpan’s internal state, achieve arbitrary out-of-bounds writes during the sweep phase, and ultimately execute arbitrary code within the renderer process.

Suggested Fix

There are two primary ways to fix this vulnerability:

  1. Fix the Math: Modify ObjectStartBitmap::FindHeader to safely handle the case where the bitmap is entirely zero. If byte == 0 after the loop, the function should explicitly crash (CHECK(false)) or return nullptr instead of relying on the underflowing arithmetic.
  2. Memory Ordering: Ensure that the write barrier for cppgc::Member assignment enforces sequential consistency or proper acquire-release semantics to prevent the relaxed pointer store from being reordered before the ObjectStartBitmap update.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker