CVE-2026-91731
Overview
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) whoseGetNextreturns anintfromstd::atomic_int seq_(:23,:26). - `GetNextLayerId`
cc/layers/layer.cc:97allocator that returnsg_next_layer_id.GetNext() + 1as each layer’s id.
Root Cause Analysis
Verify before building a PoC
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.
d4b3ac216668. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (7767cccfee47).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.static_casts depend on; the fix CHECKs against overflow so the counter can never wrap.Attack Path
- Exhaust the id space
Script rapidly creates and destroys compositor layers to drive
g_next_layer_idtoward its 32-bit maximum. (inferred — allocation-rate trigger from reporter) - 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. - 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)
- 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
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 countersFlag 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 downcastsAudit
static_cast/DownCastof objects fetched by an integer key for a release-build uniqueness guarantee; aDCHECK-only uniqueness check is insufficient against a wrapped counter.
Patch
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() {
Original Bug Report
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.cccc/trees/layer_tree_impl.cccc/layers/layer_collections.cccc/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:
- Map Overwrite: When the colliding
PictureLayeris added to the tree,LayerTreeHost::RegisterLayer()registers it. The uniqueness checkDCHECK(!LayerById(layer->id()));is compiled out in release builds, allowing the new layer to overwrite the old entry in the main thread’slayer_id_map. However, both layers physically remain in the layer tree. - Tree Synchronization: During commit,
TreeSynchronizer::SynchronizeTreestraverses the main thread’s tree in drawing order. It createsLayerImplobjects for both theTextureLayerand thePictureLayer, assigning them both the identical ID. Both are pushed into theLayerTreeImpl’s internal list. - Map Initialization Bug: To facilitate fast lookups,
OwnedLayerImplList::RebuildLayerMaps()creates a sorted vector of(layer_id, index)pairs. Becausestd::pairsorting is lexicographical, the layer that appeared earlier in the DOM (e.g., theTextureLayer) is stably sorted before thePictureLayer. This vector is used to initialize abase::flat_mapusing thebase::sorted_uniquetag. In release builds,base::flat_mapskips duplicate removal, silently retaining both entries. - Type Confusion: In
LayerTreeImpl::PullPropertiesFrom, the compositor iterates overcommit_state.picture_layer_ids_with_new_raster_sourceto update raster sources. When it queriesLayerById(id)for the colliding ID,base::flat_map::findusesstd::lower_bound, returning the first match. Due to the sorting order, it returns theTextureLayerImplinstead of the expectedPictureLayerImpl. - Exploitation: The returned pointer is unsafely cast:
static_cast<PictureLayerImpl*>(LayerById(layer_id))->CommitPendingRasterSource(). Accessing fields likepending_raster_source_(ascoped_refptr<RasterSource>) on the incompatibleTextureLayerImplobject 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.
- Initialize a Web Worker that rapidly instantiates
OffscreenCanvasobjects and requestsbitmaprenderercontexts to quickly increment the globalg_next_layer_id. - On the main thread, track the increments and create a target
TextureLayer(e.g., via a<canvas>element) just before the ID generator overflows. - Allow the generator to wrap around, stopping when it reaches the ID of the target layer.
- Create a
PictureLayer(e.g., a<div>withwill-change: transform). It receives the same ID as theTextureLayer. - Ensure the DOM is structured such that the
TextureLayerappears before thePictureLayerin pre-order traversal. - Trigger a compositor commit, causing the renderer to execute the type confusion.
Suggested Fix
- Upgrade the global ID generator
g_next_layer_idincc/layers/layer.ccand thelayer_id_field incc/layers/layer.hto use 64-bit integers (uint64_t) to make wraparound computationally infeasible. - Implement a
CHECK(not just aDCHECK) inLayerTreeHost::RegisterLayerto 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.