Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Compositing
DescriptionUse after free in Compositing
ComponentCompositing
Bug ClassUAF
Tracker501517520
Fix commit5625e13b837a (chromium/src) +3/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
CC_PAINT_EXPORT
cc/paint/refcounted_buffer.h
modified

Files Changed

  • cc/paint/refcounted_buffer.h
From 5625e13b837a61c8f9378e742991c894a393d705 Mon Sep 17 00:00:00 2001
From: Florin Malita <fmalita@chromium.org>
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 <sunnyps@chromium.org>
Commit-Queue: Florin Malita <fmalita@chromium.org>
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_;
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • cc/paint/paint_op.cc
  • third_party/blink/renderer/modules/canvas/canvas2d/mesh_2d_buffer.h
  • third_party/blink/renderer/modules/csspaint/paint_worklet.cc
  • third_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:

  1. When JavaScript creates a mesh buffer, a blink::Mesh2DBuffer is instantiated on the Main Thread, holding a scoped_refptr<cc::RefCountedBuffer<T>>.
  2. When ctx.drawMesh() is called, this scoped_refptr is copied and captured into a DrawVerticesOp, which is recorded into a PaintOpBuffer (and eventually a cc::PaintRecord).
  3. In scenarios like CSS PaintWorklets executing on the Main Thread, this PaintRecord is 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 Mesh2DBuffer as unreachable and executes its destructor, calling Release().
  • Concurrently, the Compositor Thread discards the PaintRecord, destroying the DrawVerticesOp and calling Release().

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 ~vector destructor will call free() on the attacker-controlled __begin_ pointer.
  • Arbitrary Read: If RasterWithFlags executes 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.)

  1. Enable the Canvas2dMesh feature (e.g., via --enable-blink-features=Canvas2dMesh).
  2. Register a CSS PaintWorklet that specifies inputProperties: ['background-image'] to force a cross-thread fallback to Main Thread execution.
  3. In the worklet’s paint() method, create vertex buffers using ctx.createMesh2DVertexBuffer() and record them using ctx.drawMesh().
  4. Rapidly invalidate the CSS paint style via requestAnimationFrame to force the Compositor Thread to frequently discard and replace PaintRecords.
  5. Simultaneously, on the Main Thread, drop JS references to the mesh buffers and create high allocation pressure to trigger frequent Oilpan GC sweeps.
  6. 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.

View on issue tracker