High watch ~ plausible CVSS 8.8 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in Compositing
DescriptionType confusion in Compositing
ComponentCompositing
Sub-componentInternals>Compositing
Bug ClassType Confusion
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Fix commit and citations (layer.cc:79/97/99/109; atomic_sequence_num.h:15/23/26) are accurate. But it gives ~2^31 for id reuse where ~2^32 is right, and attributes the static_cast lookup to the advisory instead of the reporter.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker523554372
Fix commitd4b3ac216668 (chromium/src) +4/-1
Related commits7767cccfee47 companion · chromium/src
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-13 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-15
Patch gap19 days — fix public in source before the advisory

Background

`cc::Layer`
a compositor layer object; every instance takes a process-global integer id at construction (cc/layers/layer.cc:109, layer_id_(GetNextLayerId())).
`base::AtomicSequenceNumber`
a thread-safe increasing counter (base/atomic_sequence_num.h:15) whose GetNext returns an int from std::atomic_int seq_ (:23,:26).
`GetNextLayerId`
cc/layers/layer.cc:97 allocator that returns g_next_layer_id.GetNext() + 1 as each layer’s id.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Fix commit and citations (layer.cc:79/97/99/109; atomic_sequence_num.h:15/23/26) are accurate. But it gives ~2^31 for id reuse where ~2^32 is right, and attributes the static_cast lookup to the advisory instead of the reporter.
Re-analyzed on 2026-10-03 against the actual fix commit d4b3ac216668. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (7767cccfee47).
Layer ids come from the process-global g_next_layer_id (cc/layers/layer.cc:79), an AtomicSequenceNumber backed by a signed 32-bit std::atomic_int (base/atomic_sequence_num.h:26). Pre-fix, Layer::GetNextLayerId simply returned g_next_layer_id.GetNext() + 1 (cc/layers/layer.cc:99) with no overflow guard, and this id is assigned to layer_id_ for every layer (cc/layers/layer.cc:109). The invariant that compositor layer ids are unique therefore held only until the 32-bit counter wrapped: after ~2^31 allocations the signed counter passes INT_MAX and wraps to negative values (still new ids), and only after ~2^32 total allocations does it come back around to re-issue positive ids that may still be held by live layers (the reporter cites ~4.29 billion increments), so two layers of different subclasses can share an id. The advisory states only “Type confusion in Compositing”; per the reporter (unverified, code not in the shown material), the commit path in LayerTreeImpl::PullPropertiesFrom looks a layer up by id and static_casts it to PictureLayerImpl, which on a collision is the wrong object. The primary fix changes GetNextLayerId to int prev = g_next_layer_id.GetNext(); CHECK_LT(prev, std::numeric_limits<int>::max()); return prev + 1; (added diff lines, plus #include <limits>), converting the wraparound into a deterministic crash as soon as the counter reaches INT_MAX (~2^31 allocations), well before a duplicate id can be issued. The companion commit (7767cccfee47e42ca30da74cf6d84b80537462f1) only templatizes the counter as AtomicSequenceNumberT<T> while keeping using AtomicSequenceNumber = AtomicSequenceNumberT<int> (base/atomic_sequence_num.h diff), so it is enabling infrastructure and does not itself remove the bug.
Key insight
A 32-bit layer-id counter with no overflow check could wrap and, after ~2^32 allocations, re-issue ids, breaking the uniqueness that subclass static_casts depend on; the fix CHECKs against overflow so the counter can never wrap.

Attack Path

  1. Exhaust the id space Script rapidly creates and destroys compositor layers to drive g_next_layer_id toward its 32-bit maximum. (inferred — allocation-rate trigger from reporter)
  2. Wrap the counter Without the overflow guard, GetNextLayerId (cc/layers/layer.cc:99) wraps through negative values and, after ~2^32 allocations, returns an id already owned by a live layer of a different subclass.
  3. Collide ids across subclasses Two layers now share one id, so an id-keyed lookup can return a layer of the unexpected type. (inferred — lookup code not in shown material)
  4. Trigger the bad cast A later compositor operation static_casts the looked-up layer to the assumed subclass and accesses mismatched fields, the advisory’s type confusion. (inferred)

Impact Assessment

An attacker running script in the renderer could, after forcing ~2^32 layer allocations (per the reporter’s ~4.29 billion figure), cause compositor layer-id collisions that lead to a type confusion in the cc compositing code and potential memory corruption in the renderer process. The advisory rates this high severity with no in-the-wild exploitation; progression from the type confusion to code execution is (inferred).

Files Changed

  • cc/layers/layer.cc

Audit Directions

  • Unchecked identifier counters
    Flag global/atomic id or sequence generators whose increment has no saturation or overflow check, especially where ids are assumed unique for the object’s lifetime.
  • Id-keyed downcasts
    Audit static_cast/DownCast of objects fetched by an integer key for a release-build uniqueness guarantee; a DCHECK-only uniqueness check is insufficient against a wrapped counter.
diff --git a/cc/layers/layer.cc b/cc/layers/layer.cc
index 7255a87f..eb8abf71 100644
--- a/cc/layers/layer.cc
+++ b/cc/layers/layer.cc
@@ -9,6 +9,7 @@
 
 #include <algorithm>
 #include <cmath>
+#include <limits>
 #include <string>
 #include <unordered_set>
 #include <utility>
@@ -96,7 +97,9 @@
 
 int Layer::GetNextLayerId() {
   // Layer IDs start from 1.
-  return g_next_layer_id.GetNext() + 1;
+  int prev = g_next_layer_id.GetNext();
+  CHECK_LT(prev, std::numeric_limits<int>::max());
+  return prev + 1;
 }
 
 scoped_refptr<Layer> Layer::Create() {
Loading diff…

Original Bug Report

reported by [email protected]

Potential Class Hierarchy Type Confusion in cc::LayerTreeImpl via 32-bit Layer ID Wraparound

Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A 32-bit integer wraparound in the global layer ID generator allows an attacker to cause ID collisions between different cc::Layer subclasses. During synchronization, the compositor thread performs an unsafe static_cast on a collided ID, leading to type confusion and potential memory corruption in the renderer process.

Affected files:

  • cc/layers/layer.cc
  • cc/trees/layer_tree_impl.cc
  • cc/layers/layer_collections.cc
  • cc/layers/layer.h

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential class hierarchy type confusion vulnerability exists in the Chrome Compositor (cc) due to a 32-bit integer wraparound in the global layer ID generator. By rapidly creating and destroying layers, an attacker can wrap the ID counter and cause newly created layers to share an ID with existing layers of a different subclass. During the compositor commit process, this duplicate ID causes LayerTreeImpl to retrieve the wrong layer object, leading to an unsafe static_cast and potential arbitrary code execution within the renderer process.

Vulnerability Details

Every compositor layer (cc::Layer) is assigned a unique ID upon creation using a global base::AtomicSequenceNumber defined in cc/layers/layer.cc. This generator utilizes a 32-bit signed integer (std::atomic_int), which wraps around after approximately 4.29 billion increments.

When the counter wraps, an attacker can cause a newly created layer (e.g., a PictureLayer) to be assigned the same ID as an existing, long-lived layer of a different subclass (e.g., a TextureLayer).

The vulnerability unfolds as follows:

  1. Map Overwrite: When the colliding PictureLayer is added to the tree, LayerTreeHost::RegisterLayer() registers it. The uniqueness check DCHECK(!LayerById(layer->id())); is compiled out in release builds, allowing the new layer to overwrite the old entry in the main thread’s layer_id_map. However, both layers physically remain in the layer tree.
  2. Tree Synchronization: During commit, TreeSynchronizer::SynchronizeTrees traverses the main thread’s tree in drawing order. It creates LayerImpl objects for both the TextureLayer and the PictureLayer, assigning them both the identical ID. Both are pushed into the LayerTreeImpl’s internal list.
  3. Map Initialization Bug: To facilitate fast lookups, OwnedLayerImplList::RebuildLayerMaps() creates a sorted vector of (layer_id, index) pairs. Because std::pair sorting is lexicographical, the layer that appeared earlier in the DOM (e.g., the TextureLayer) is stably sorted before the PictureLayer. This vector is used to initialize a base::flat_map using the base::sorted_unique tag. In release builds, base::flat_map skips duplicate removal, silently retaining both entries.
  4. Type Confusion: In LayerTreeImpl::PullPropertiesFrom, the compositor iterates over commit_state.picture_layer_ids_with_new_raster_source to update raster sources. When it queries LayerById(id) for the colliding ID, base::flat_map::find uses std::lower_bound, returning the first match. Due to the sorting order, it returns the TextureLayerImpl instead of the expected PictureLayerImpl.
  5. Exploitation: The returned pointer is unsafely cast: static_cast<PictureLayerImpl*>(LayerById(layer_id))->CommitPendingRasterSource(). Accessing fields like pending_raster_source_ (a scoped_refptr<RasterSource>) on the incompatible TextureLayerImpl object leads to overlapping memory access and potential hijacked virtual method calls.

Suggested Steps to Trigger (Theoretical)

Note: These are potential steps; our tooling agent does not execute code to provide a working PoC.

  1. Initialize a Web Worker that rapidly instantiates OffscreenCanvas objects and requests bitmaprenderer contexts to quickly increment the global g_next_layer_id.
  2. On the main thread, track the increments and create a target TextureLayer (e.g., via a <canvas> element) just before the ID generator overflows.
  3. Allow the generator to wrap around, stopping when it reaches the ID of the target layer.
  4. Create a PictureLayer (e.g., a <div> with will-change: transform). It receives the same ID as the TextureLayer.
  5. Ensure the DOM is structured such that the TextureLayer appears before the PictureLayer in pre-order traversal.
  6. Trigger a compositor commit, causing the renderer to execute the type confusion.

Suggested Fix

  1. Upgrade the global ID generator g_next_layer_id in cc/layers/layer.cc and the layer_id_ field in cc/layers/layer.h to use 64-bit integers (uint64_t) to make wraparound computationally infeasible.
  2. Implement a CHECK (not just a DCHECK) in LayerTreeHost::RegisterLayer to enforce ID uniqueness in release builds, preventing map collisions outright.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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