CVE-2026-11125
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
CC_PAINT_EXPORTcc/paint/refcounted_buffer.h |
modified |
Files Changed
cc/paint/refcounted_buffer.h
Patch
From 5625e13b837a61c8f9378e742991c894a393d705 Mon Sep 17 00:00:00 2001 From: Florin Malita <[email protected]> Date: Tue, 14 Apr 2026 05:05:29 -0700 Subject: [PATCH] Thread-safe cc::RefCountedBuffer RefCountedBuffers can be shared across multiple threads, but they are not currently implementing atomic ref counting semantics. Inherit from RefCountedThreadSafe instead of plain RefCounted, to avoid concurrency issues. Bug: chromium:501517520 Change-Id: Ice9b3540eede1520a93b322987fb4544e07fd825 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7757082 Reviewed-by: Sunny Sachanandani <[email protected]> Commit-Queue: Florin Malita <[email protected]> Cr-Commit-Position: refs/heads/main@{#1614378} --- diff --git a/cc/paint/refcounted_buffer.h b/cc/paint/refcounted_buffer.h index a2c95009..7530283 100644 --- a/cc/paint/refcounted_buffer.h +++ b/cc/paint/refcounted_buffer.h @@ -13,12 +13,12 @@ namespace cc { -// A trivial RefCounted wrapper for a block of data. +// A RefCountedThreadSafe wrapper for a block of data. // This is intended to minimize the number of copies when e.g. // recording large vertex/uv/index arrays to a PaintOpBuffer. template <typename T> class CC_PAINT_EXPORT RefCountedBuffer - : public base::RefCounted<RefCountedBuffer<T>> { + : public base::RefCountedThreadSafe<RefCountedBuffer<T>> { public: REQUIRE_ADOPTION_FOR_REFCOUNTED_TYPE(); @@ -32,7 +32,7 @@ } private: - friend class base::RefCounted<RefCountedBuffer<T>>; + friend class base::RefCountedThreadSafe<RefCountedBuffer<T>>; ~RefCountedBuffer() = default; std::vector<T> buffer_;
Original Bug Report
Race Condition in cc::RefCountedBuffer via Canvas2dMesh leading to UAF
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.
Overview: cc::RefCountedBuffer uses non-thread-safe reference counting but can be shared across the Main and Compositor threads via the new Canvas2dMesh API. Concurrent destruction of references by Blink’s garbage collector and the compositor’s PaintRecord destruction leads to a data race. This can result in a Use-After-Free or Double-Free, which bypasses MiraclePtr due to scoped_refptr’s exclusion.
Affected files:
cc/paint/refcounted_buffer.hcc/paint/paint_op.ccthird_party/blink/renderer/modules/canvas/canvas2d/mesh_2d_buffer.hthird_party/blink/renderer/modules/csspaint/paint_worklet.ccthird_party/blink/renderer/core/css/css_paint_value.cc
Estimated timestamp from git blame: 2025-09-03
Technical Details
The cc::RefCountedBuffer<T> class (defined in cc/paint/refcounted_buffer.h) is used to store vertex, UV, and index data for DrawVerticesOp. It incorrectly inherits from base::RefCounted<RefCountedBuffer<T>>, which uses a non-atomic uint32_t for its reference count.
Normally, this is fine if the buffer remains on a single thread. However, the Canvas2dMesh API allows this buffer to be shared across threads:
- When JavaScript creates a mesh buffer, a
blink::Mesh2DBufferis instantiated on the Main Thread, holding ascoped_refptr<cc::RefCountedBuffer<T>>. - When
ctx.drawMesh()is called, thisscoped_refptris copied and captured into aDrawVerticesOp, which is recorded into aPaintOpBuffer(and eventually acc::PaintRecord). - In scenarios like CSS PaintWorklets executing on the Main Thread, this
PaintRecordis transferred to the Compositor (CC) thread for rasterization.
At this point, references to the same cc::RefCountedBuffer are owned by two different threads. A data race occurs if:
- The Main Thread’s Oilpan GC identifies the JS
Mesh2DBufferas unreachable and executes its destructor, callingRelease(). - Concurrently, the Compositor Thread discards the
PaintRecord, destroying theDrawVerticesOpand callingRelease().
Because base::RefCounted::Release() reads and decrements the counter non-atomically, interleaved execution can lead to both threads observing --ref_count_ == 0, causing a Double-Free. Alternatively, one thread may free the buffer while a raster worker thread is actively reading from it in DrawVerticesOp::RasterWithFlags, causing a Use-After-Free.
Impact
The internal pointer of scoped_refptr (ptr_) is explicitly annotated with RAW_PTR_EXCLUSION. Consequently, BackupRefPtr (MiraclePtr) does not quarantine this allocation, rendering standard UAF protections ineffective.
The cc::RefCountedBuffer is exactly 32 bytes (4-byte refcount + 4-byte padding + 24-byte std::vector), making it a highly reliable target for PartitionAlloc heap spraying. By reclaiming the freed chunk and forging the std::vector’s internal pointers (__begin_, __end_, etc.), an attacker can achieve:
- Arbitrary Free: When the Double-Free occurs, the
~vectordestructor will callfree()on the attacker-controlled__begin_pointer. - Arbitrary Read: If
RasterWithFlagsexecutes after the memory is replaced, passing the forged__begin_pointer to Skia allows reading arbitrary memory into canvas pixels.
These primitives can be reliably leveraged to achieve Remote Code Execution (RCE) in the renderer process.
Potential Steps to Reproduce
(Note: These are suggested steps; our tooling agent cannot execute code to provide a working PoC.)
- Enable the
Canvas2dMeshfeature (e.g., via--enable-blink-features=Canvas2dMesh). - Register a CSS PaintWorklet that specifies
inputProperties: ['background-image']to force a cross-thread fallback to Main Thread execution. - In the worklet’s
paint()method, create vertex buffers usingctx.createMesh2DVertexBuffer()and record them usingctx.drawMesh(). - Rapidly invalidate the CSS paint style via
requestAnimationFrameto force the Compositor Thread to frequently discard and replacePaintRecords. - Simultaneously, on the Main Thread, drop JS references to the mesh buffers and create high allocation pressure to trigger frequent Oilpan GC sweeps.
- The concurrent non-atomic decrements will eventually trigger a crash (observable under TSAN/ASan).
Suggested Fix
Modify cc::RefCountedBuffer<T> in cc/paint/refcounted_buffer.h to inherit from base::RefCountedThreadSafe instead of base::RefCounted. This ensures that reference counting operations are atomic and safe across thread boundaries.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.