Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Compositing
DescriptionUse after free in Compositing
ComponentCompositing
Sub-componentCc
Bug ClassUAF
Tracker534956586
Fix commitcfaf038e7459 (chromium/src) +163/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
cc/tiles/tile_manager.cc
modified
if
cc/tiles/tile_manager_unittest.cc
modified
TileManagerDeferredRedrawPendingGpuWorkTest
cc/tiles/tile_manager_unittest.cc
modified
BindLambdaForTesting
cc/tiles/tile_manager_unittest.cc
modified
for
cc/tiles/tile_manager_unittest.cc
modified

Files Changed

  • cc/tiles/tile_manager_unittest.cc
  • cc/tiles/tile_manager.cc
From cfaf038e745952481eb0ed0b2a2ea641687fbecc Mon Sep 17 00:00:00 2001
From: Victor Miura <vmiura@chromium.org>
Date: Mon, 21 Sep 2026 15:44:26 -0700
Subject: [PATCH] cc: Defer SetNeedsRedraw while checking pending GPU work tiles

CheckPendingGpuWorkAndIssueSignals() notifies the client of each
newly-ready tile with set_needs_redraw=true while it is still iterating
pending_gpu_work_tiles_ and collecting pointers to the remaining tiles'
resources for SetReadyToDrawCallback(). Requesting a redraw there can
synchronously re-enter Scheduler::ProcessScheduledActions(), which may
activate the sync tree and remove tiles that the loop iterator and the
collected resource lists still reference.

Apply the same pattern already used by MarkTilesOutOfMemory() and
PrepareTiles(): notify each tile without requesting a redraw,
accumulate whether any newly-ready tile was required for draw, and
request a single redraw once the iteration is complete and the resource
lists have been consumed.

Bug: 534956586
Change-Id: I2cd58006680c8ede0d95d78ac40fbb7aeb938cbd
Fixed: 534956586
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8431892
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Auto-Submit: Victor Miura <vmiura@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702131}
---
diff --git a/cc/tiles/tile_manager.cc b/cc/tiles/tile_manager.cc
index 424105b..c790d38 100644
--- a/cc/tiles/tile_manager.cc
+++ b/cc/tiles/tile_manager.cc
@@ -2130,6 +2130,7 @@
 
   std::vector<const ResourcePool::InUsePoolResource*> required_for_activation;
   std::vector<const ResourcePool::InUsePoolResource*> required_for_draw;
+  bool ready_tile_required_for_draw = false;
 
   for (auto it = pending_gpu_work_tiles_.begin();
        it != pending_gpu_work_tiles_.end();) {
@@ -2147,7 +2148,8 @@
         raster_buffer_provider_->IsResourceReadyToDraw(resource)) {
       tile->draw_info().set_resource_ready_for_draw();
       client_->NotifyTileStateChanged(tile, /*update_damage=*/true,
-                                      /*set_needs_redraw=*/true);
+                                      /*set_needs_redraw=*/false);
+      ready_tile_required_for_draw |= tile->required_for_draw();
       it = pending_gpu_work_tiles_.erase(it);
       continue;
     }
@@ -2187,6 +2189,18 @@
             pending_required_for_draw_callback_id_);
   }
 
+  // Similar to MarkTilesOutOfMemory(), if we trigger SetNeedsRedraw() while
+  // iterating pending_gpu_work_tiles_, we may end up triggering
+  // Scheduler::ProcessScheduledActions(), which is inefficient and may in turn
+  // trigger ActivateSyncTree() and other actions that remove tiles still
+  // referenced by the loop iterator and the resource lists above, leading to
+  // UAF (crbug.com/534956586). Defer the redraw until the iteration is
+  // complete and the resource lists have been consumed.
+  if (ready_tile_required_for_draw) {
+    client_->SetNeedsRedraw(/*animation_only=*/false,
+                            /*skip_if_inside_draw=*/true);
+  }
+
   // Update our signals now that we know whether we have pending resources.
   signals_.activate_gpu_work_completed =
       (pending_required_for_activation_callback_id_ == 0);
diff --git a/cc/tiles/tile_manager_unittest.cc b/cc/tiles/tile_manager_unittest.cc
index 6b59c7c6..e359f2ec 100644
--- a/cc/tiles/tile_manager_unittest.cc
+++ b/cc/tiles/tile_manager_unittest.cc
@@ -15,6 +15,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
@@ -64,6 +65,7 @@
 #include "third_party/skia/include/core/SkSurface.h"
 
 using testing::_;
+using testing::NiceMock;
 using testing::Return;
 using testing::StrictMock;
 
@@ -1911,12 +1913,19 @@
     void NotifyTileStateChanged(const Tile* tile,
                                 bool update_damage,
                                 bool set_needs_redraw) override {
-      if (set_needs_redraw && tilings_to_remove_) {
+      if (!set_needs_redraw) {
+        return;
+      }
+      if (tilings_to_remove_) {
         ++notify_with_redraw_count_;
         PictureLayerTilingSet* tilings = tilings_to_remove_;
         tilings_to_remove_ = nullptr;
         tilings->RemoveAllTilings();
       }
+      if (on_notify_with_redraw_) {
+        ++notify_with_redraw_count_;
+        std::move(on_notify_with_redraw_).Run(tile);
+      }
     }
 
     void SetNeedsRedraw(bool animation_only,
@@ -1927,11 +1936,16 @@
     void set_tilings_to_remove(PictureLayerTilingSet* tilings) {
       tilings_to_remove_ = tilings;
     }
+    void set_on_notify_with_redraw(
+        base::OnceCallback<void(const Tile*)> callback) {
+      on_notify_with_redraw_ = std::move(callback);
+    }
     int notify_with_redraw_count() const { return notify_with_redraw_count_; }
     int set_needs_redraw_count() const { return set_needs_redraw_count_; }
 
    private:
     raw_ptr<PictureLayerTilingSet> tilings_to_remove_ = nullptr;
+    base::OnceCallback<void(const Tile*)> on_notify_with_redraw_;
     int notify_with_redraw_count_ = 0;
     int set_needs_redraw_count_ = 0;
   };
@@ -3229,6 +3243,139 @@
   EXPECT_FALSE(host_impl()->is_likely_to_require_a_draw());
 }
 
+// Combines TileManagerDeferredRedrawTest's re-entrant host impl with a mock
+// raster buffer provider so that tiles waiting on GPU work can be simulated.
+class TileManagerDeferredRedrawPendingGpuWorkTest
+    : public TileManagerDeferredRedrawTest {
+ public:
+  void SetUp() override {
+    TileManagerDeferredRedrawTest::SetUp();
+    host_impl()->tile_manager()->SetRasterBufferProviderForTesting(
+        &mock_raster_buffer_provider_);
+  }
+
+  void SetupActiveTreeWithTiles() {
+    host_impl()->SetTreePriority(SMOOTHNESS_TAKES_PRIORITY);
+    const gfx::Size layer_bounds(1000, 1000);
+    host_impl()->active_tree()->SetDeviceViewportRect(gfx::Rect(layer_bounds));
+    SetupDefaultTrees(layer_bounds);
+    // Activate so that only the active tree's tiles are rastered below.
+    ActivateTree();
+  }
+
+  // Rasters the active tree's tiles while IsResourceReadyToDraw() reports
+  // false, so that each of them ends up waiting on GPU work, and returns the
+  // ready-to-draw callback that TileManager registers to re-check them.
+  base::OnceClosure RasterTilesWithPendingGpuWork() {
+    base::OnceClosure ready_check;
+    base::RunLoop run_loop;
+    EXPECT_CALL(mock_raster_buffer_provider_, IsResourceReadyToDraw(_))
+        .WillRepeatedly(Return(false));
+    EXPECT_CALL(mock_raster_buffer_provider_, SetReadyToDrawCallback(_, _, _))
+        .WillRepeatedly(
+            [&](const std::vector<const ResourcePool::InUsePoolResource*>&,
+                base::OnceClosure callback, uint64_t) {
+              if (!ready_check) {
+                ready_check = std::move(callback);
+                run_loop.Quit();
+              }
+              return 1;
+            });
+    host_impl()->tile_manager()->PrepareTiles(host_impl()->global_tile_state());
+    run_loop.Run();
+    testing::Mock::VerifyAndClearExpectations(&mock_raster_buffer_provider_);
+    return ready_check;
+  }
+
+  MockReadyToDrawRasterBufferProviderImpl* mock_raster_buffer_provider() {
+    return &mock_raster_buffer_provider_;
+  }
+
+ private:
+  NiceMock<MockReadyToDrawRasterBufferProviderImpl>
+      mock_raster_buffer_provider_;
+};
+
+TEST_F(TileManagerDeferredRedrawPendingGpuWorkTest,
+       DeferRedrawWhileCheckingPendingGpuWork) {
+  SetupActiveTreeWithTiles();
+
+  base::OnceClosure ready_check = RasterTilesWithPendingGpuWork();
+  ASSERT_TRUE(ready_check);
+
+  PictureLayerTiling* tiling = active_layer()->HighResTiling();
+  ASSERT_GT(tiling->AllTilesForTesting().size(), 1u);
+
+  // On the next check, keep the first queried resource waiting on GPU work so
+  // that it is retained for the next ready-to-draw callback registration, and
+  // report every other resource as ready so that the client is notified of
+  // their tiles' state changes mid-iteration.
+  int ready_queries = 0;
+  EXPECT_CALL(*mock_raster_buffer_provider(), IsResourceReadyToDraw(_))
+      .WillRepeatedly([&ready_queries](const ResourcePool::InUsePoolResource&) {
+        return ++ready_queries > 1;
+      });
+
+  // A tile state change notification that requests a redraw mid-iteration may
+  // re-enter the client, which can evict any other tile - including the one
+  // whose resource is retained above.
+  reentrant_host_impl()->set_on_notify_with_redraw(
+      base::BindLambdaForTesting([&](const Tile* tile) {
+        gfx::Rect keep(tile->content_rect().CenterPoint(), gfx::Size(1, 1));
+        tiling->SetTilePriorityRectsForTesting(keep, keep, keep, keep,
+                                               /*evicts_tiles=*/true);
+      }));
+
+  // The resources handed to the ready-to-draw callback registration must all
+  // still be alive.
+  bool ready_to_draw_callback_set = false;
+  EXPECT_CALL(*mock_raster_buffer_provider(), SetReadyToDrawCallback(_, _, _))
+      .WillRepeatedly(
+          [&ready_to_draw_callback_set](
+              const std::vector<const ResourcePool::InUsePoolResource*>&
+                  resources,
+              base::OnceClosure, uint64_t) {
+            ready_to_draw_callback_set = true;
+            for (const ResourcePool::InUsePoolResource* in_use : resources) {
+              EXPECT_TRUE(in_use->backing()->mailbox_sync_token.HasData());
+            }
+            return 2;
+          });
+
+  const int redraw_count = reentrant_host_impl()->set_needs_redraw_count();
+  std::move(ready_check).Run();
+
+  // Tile state change notifications must not request a redraw while the tiles
+  // waiting on GPU work are being iterated; a single SetNeedsRedraw() should
+  // be issued afterwards instead.
+  EXPECT_EQ(0, reentrant_host_impl()->notify_with_redraw_count());
+  EXPECT_LT(redraw_count, reentrant_host_impl()->set_needs_redraw_count());
+  EXPECT_TRUE(ready_to_draw_callback_set);
+}
+
+TEST_F(TileManagerDeferredRedrawPendingGpuWorkTest,
+       DeferRedrawWhenAllPendingGpuWorkTilesBecomeReady) {
+  SetupActiveTreeWithTiles();
+
+  base::OnceClosure ready_check = RasterTilesWithPendingGpuWork();
+  ASSERT_TRUE(ready_check);
+  ASSERT_GT(active_layer()->HighResTiling()->AllTilesForTesting().size(), 1u);
+
+  // Every resource becomes ready on the next check. Arm the host impl to
+  // remove the active tilings if it observes a tile state change notification
+  // that requests a redraw while the tiles waiting on GPU work are being
+  // iterated.
+  EXPECT_CALL(*mock_raster_buffer_provider(), IsResourceReadyToDraw(_))
+      .WillRepeatedly(Return(true));
+  reentrant_host_impl()->set_tilings_to_remove(active_layer()->tilings());
+
+  const int redraw_count = reentrant_host_impl()->set_needs_redraw_count();
+  std::move(ready_check).Run();
+
+  EXPECT_EQ(0, reentrant_host_impl()->notify_with_redraw_count());
+  EXPECT_LT(redraw_count, reentrant_host_impl()->set_needs_redraw_count());
+}
+
 PaintImage MakeCheckerablePaintImage(const gfx::Size& size) {
   auto image = CreateDiscardablePaintImage(size);
   return PaintImageBuilder::WithCopy(image)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/cc/tiles/tile_manager_unittest.cc b/cc/tiles/tile_manager_unittest.cc
index 6b59c7c6..e359f2ec 100644
--- a/cc/tiles/tile_manager_unittest.cc
+++ b/cc/tiles/tile_manager_unittest.cc
@@ -15,6 +15,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
@@ -64,6 +65,7 @@
 #include "third_party/skia/include/core/SkSurface.h"
 
 using testing::_;
+using testing::NiceMock;
 using testing::Return;
 using testing::StrictMock;
 
@@ -1911,12 +1913,19 @@
     void NotifyTileStateChanged(const Tile* tile,
                                 bool update_damage,
                                 bool set_needs_redraw) override {
-      if (set_needs_redraw && tilings_to_remove_) {
+      if (!set_needs_redraw) {
+        return;
+      }
+      if (tilings_to_remove_) {
         ++notify_with_redraw_count_;
         PictureLayerTilingSet* tilings = tilings_to_remove_;
         tilings_to_remove_ = nullptr;
         tilings->RemoveAllTilings();
       }
+      if (on_notify_with_redraw_) {
+        ++notify_with_redraw_count_;
+        std::move(on_notify_with_redraw_).Run(tile);
+      }
     }
 
     void SetNeedsRedraw(bool animation_only,
@@ -1927,11 +1936,16 @@
     void set_tilings_to_remove(PictureLayerTilingSet* tilings) {
       tilings_to_remove_ = tilings;
     }
+    void set_on_notify_with_redraw(
+        base::OnceCallback<void(const Tile*)> callback) {
+      on_notify_with_redraw_ = std::move(callback);
+    }
     int notify_with_redraw_count() const { return notify_with_redraw_count_; }
     int set_needs_redraw_count() const { return set_needs_redraw_count_; }
 
    private:
     raw_ptr<PictureLayerTilingSet> tilings_to_remove_ = nullptr;
+    base::OnceCallback<void(const Tile*)> on_notify_with_redraw_;
     int notify_with_redraw_count_ = 0;
     int set_needs_redraw_count_ = 0;
   };
@@ -3229,6 +3243,139 @@
   EXPECT_FALSE(host_impl()->is_likely_to_require_a_draw());
 }
 
+// Combines TileManagerDeferredRedrawTest's re-entrant host impl with a mock
+// raster buffer provider so that tiles waiting on GPU work can be simulated.
+class TileManagerDeferredRedrawPendingGpuWorkTest
+    : public TileManagerDeferredRedrawTest {
+ public:
+  void SetUp() override {
+    TileManagerDeferredRedrawTest::SetUp();
+    host_impl()->tile_manager()->SetRasterBufferProviderForTesting(
+        &mock_raster_buffer_provider_);
+  }
+
+  void SetupActiveTreeWithTiles() {
+    host_impl()->SetTreePriority(SMOOTHNESS_TAKES_PRIORITY);
+    const gfx::Size layer_bounds(1000, 1000);
+    host_impl()->active_tree()->SetDeviceViewportRect(gfx::Rect(layer_bounds));
+    SetupDefaultTrees(layer_bounds);
+    // Activate so that only the active tree's tiles are rastered below.
+    ActivateTree();
+  }
+
+  // Rasters the active tree's tiles while IsResourceReadyToDraw() reports
+  // false, so that each of them ends up waiting on GPU work, and returns the
+  // ready-to-draw callback that TileManager registers to re-check them.
+  base::OnceClosure RasterTilesWithPendingGpuWork() {
+    base::OnceClosure ready_check;
+    base::RunLoop run_loop;
+    EXPECT_CALL(mock_raster_buffer_provider_, IsResourceReadyToDraw(_))
+        .WillRepeatedly(Return(false));
+    EXPECT_CALL(mock_raster_buffer_provider_, SetReadyToDrawCallback(_, _, _))
+        .WillRepeatedly(
+            [&](const std::vector<const ResourcePool::InUsePoolResource*>&,
+                base::OnceClosure callback, uint64_t) {
+              if (!ready_check) {
+                ready_check = std::move(callback);
+                run_loop.Quit();
+              }
+              return 1;
+            });
+    host_impl()->tile_manager()->PrepareTiles(host_impl()->global_tile_state());
+    run_loop.Run();
+    testing::Mock::VerifyAndClearExpectations(&mock_raster_buffer_provider_);
+    return ready_check;
+  }
+
+  MockReadyToDrawRasterBufferProviderImpl* mock_raster_buffer_provider() {
+    return &mock_raster_buffer_provider_;
+  }
+
+ private:
+  NiceMock<MockReadyToDrawRasterBufferProviderImpl>
+      mock_raster_buffer_provider_;
+};
+
+TEST_F(TileManagerDeferredRedrawPendingGpuWorkTest,
+       DeferRedrawWhileCheckingPendingGpuWork) {
+  SetupActiveTreeWithTiles();
+
+  base::OnceClosure ready_check = RasterTilesWithPendingGpuWork();
+  ASSERT_TRUE(ready_check);
+
+  PictureLayerTiling* tiling = active_layer()->HighResTiling();
+  ASSERT_GT(tiling->AllTilesForTesting().size(), 1u);
+
+  // On the next check, keep the first queried resource waiting on GPU work so
+  // that it is retained for the next ready-to-draw callback registration, and
+  // report every other resource as ready so that the client is notified of
+  // their tiles' state changes mid-iteration.
+  int ready_queries = 0;
+  EXPECT_CALL(*mock_raster_buffer_provider(), IsResourceReadyToDraw(_))
+      .WillRepeatedly([&ready_queries](const ResourcePool::InUsePoolResource&) {
+        return ++ready_queries > 1;
+      });
+
+  // A tile state change notification that requests a redraw mid-iteration may
+  // re-enter the client, which can evict any other tile - including the one
+  // whose resource is retained above.
+  reentrant_host_impl()->set_on_notify_with_redraw(
+      base::BindLambdaForTesting([&](const Tile* tile) {
+        gfx::Rect keep(tile->content_rect().CenterPoint(), gfx::Size(1, 1));
+        tiling->SetTilePriorityRectsForTesting(keep, keep, keep, keep,
+                                               /*evicts_tiles=*/true);
+      }));
+
+  // The resources handed to the ready-to-draw callback registration must all
+  // still be alive.
+  bool ready_to_draw_callback_set = false;
+  EXPECT_CALL(*mock_raster_buffer_provider(), SetReadyToDrawCallback(_, _, _))
+      .WillRepeatedly(
+          [&ready_to_draw_callback_set](
+              const std::vector<const ResourcePool::InUsePoolResource*>&
+                  resources,
+              base::OnceClosure, uint64_t) {
+            ready_to_draw_callback_set = true;
+            for (const ResourcePool::InUsePoolResource* in_use : resources) {
+              EXPECT_TRUE(in_use->backing()->mailbox_sync_token.HasData());
+            }
+            return 2;
+          });
+
+  const int redraw_count = reentrant_host_impl()->set_needs_redraw_count();
+  std::move(ready_check).Run();
+
+  // Tile state change notifications must not request a redraw while the tiles
+  // waiting on GPU work are being iterated; a single SetNeedsRedraw() should
+  // be issued afterwards instead.
+  EXPECT_EQ(0, reentrant_host_impl()->notify_with_redraw_count());
+  EXPECT_LT(redraw_count, reentrant_host_impl()->set_needs_redraw_count());
+  EXPECT_TRUE(ready_to_draw_callback_set);
+}
+
+TEST_F(TileManagerDeferredRedrawPendingGpuWorkTest,
+       DeferRedrawWhenAllPendingGpuWorkTilesBecomeReady) {
+  SetupActiveTreeWithTiles();
+
+  base::OnceClosure ready_check = RasterTilesWithPendingGpuWork();
+  ASSERT_TRUE(ready_check);
+  ASSERT_GT(active_layer()->HighResTiling()->AllTilesForTesting().size(), 1u);
+
+  // Every resource becomes ready on the next check. Arm the host impl to
+  // remove the active tilings if it observes a tile state change notification
+  // that requests a redraw while the tiles waiting on GPU work are being
+  // iterated.
+  EXPECT_CALL(*mock_raster_buffer_provider(), IsResourceReadyToDraw(_))
+      .WillRepeatedly(Return(true));
+  reentrant_host_impl()->set_tilings_to_remove(active_layer()->tilings());
+
+  const int redraw_count = reentrant_host_impl()->set_needs_redraw_count();
+  std::move(ready_check).Run();
+
+  EXPECT_EQ(0, reentrant_host_impl()->notify_with_redraw_count());
+  EXPECT_LT(redraw_count, reentrant_host_impl()->set_needs_redraw_count());
+}
+
 PaintImage MakeCheckerablePaintImage(const gfx::Size& size) {
   auto image = CreateDiscardablePaintImage(size);
   return PaintImageBuilder::WithCopy(image)
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF in TileManager::CheckPendingGpuWorkAndIssueSignals via re-entrant sync tree activation

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

Overview: A potential Use-After-Free (UAF) vulnerability exists in the compositor thread of the Chrome renderer process during tile management. The function TileManager::CheckPendingGpuWorkAndIssueSignals stores raw pointers to InUsePoolResource subobjects within Tile heap allocations inside stack-allocated vectors, then synchronously invokes scheduler actions mid-loop via NotifyTileStateChanged. This can trigger sync tree activation, destroying the underlying tiles and leaving dangling pointers in the vectors which are subsequently dereferenced.

Affected files:

  • cc/tiles/tile_manager.cc
  • cc/raster/gpu_raster_buffer_provider.cc
  • cc/raster/one_copy_raster_buffer_provider.cc

Estimated timestamp from git blame: 2018-01-18

Root Cause Analysis

In cc/tiles/tile_manager.cc, the function TileManager::CheckPendingGpuWorkAndIssueSignals checks the status of pending GPU work for tiles. During this check, it builds two local stack-allocated vectors of bare pointers pointing to InUsePoolResource subobjects:

// cc/tiles/tile_manager.cc
std::vector<const ResourcePool::InUsePoolResource*> required_for_activation;
std::vector<const ResourcePool::InUsePoolResource*> required_for_draw;

for (auto it = pending_gpu_work_tiles_.begin();
     it != pending_gpu_work_tiles_.end();) {
  Tile* tile = *it;
  DCHECK(tile->draw_info().has_resource());
  const ResourcePool::InUsePoolResource& resource =
      tile->draw_info().GetResource();
  ...
  if (global_state_.tree_priority != SMOOTHNESS_TAKES_PRIORITY ||
      raster_buffer_provider_->IsResourceReadyToDraw(resource)) {
    tile->draw_info().set_resource_ready_for_draw();
    client_->NotifyTileStateChanged(tile, /*update_damage=*/true,
                                    /*set_needs_redraw=*/true);   // <--- Re-entrancy Hazard
    it = pending_gpu_work_tiles_.erase(it);
    continue;
  }
  if (tile->required_for_activation())
    required_for_activation.push_back(&resource);                 // <--- Caching raw address
  if (tile->required_for_draw())
    required_for_draw.push_back(&resource);                       // <--- Caching raw address
  ++it;
}

The variable resource is a reference to the TileDrawInfo::resource_ member variable, which is embedded inside the Tile object itself (cc/tiles/tile.h:201 and cc/tiles/tile_draw_info.h:112). Therefore, &resource is a raw C++ pointer directly into the Tile heap allocation.

Inside the loop, if a tile is ready to draw, client_->NotifyTileStateChanged is called with set_needs_redraw=true. If this task executes outside of a scheduler action (such as via a posted task from signals_check_notifier_ or a weak pointer callback from SignalSyncToken), the call synchronously re-enters the scheduler: NotifyTileStateChanged -> SetNeedsRedraw -> ProxyImpl::SetNeedsRedrawOnImplThread -> Scheduler::SetNeedsRedraw -> ProcessScheduledActions().

Since the recursion guards are not active, ProcessScheduledActions runs and can decide to dispatch ACTIVATE_SYNC_TREE if a pending tree is ready (or if we can abort the current frame because of visibility or pauses). This synchronous activation destroys obsolete active-tree tilings and their tiles, including previously iterated tiles whose resource addresses have already been cached in required_for_draw or required_for_activation.

When a tile is destroyed, it calls ~Tile(), which synchronously invokes TileManager::Release -> FreeResourcesForTile, freeing the underlying resource and the tile heap memory. The cached stack-allocated vectors are left with dangling pointers pointing to the freed memory.

Once the loop finishes, the provider dereferences the dangling pointers:

// cc/raster/gpu_raster_buffer_provider.cc
for (const auto* in_use : resources) {
  const gpu::SyncToken& sync_token = in_use->backing()->mailbox_sync_token; // <--- UAF Dereference
  ...
}

Lack of Mitigations

  • MiraclePtr: The local vectors hold plain const T* pointers, and InUsePoolResource::resource_ is explicitly marked as RAW_PTR_EXCLUSION for performance reasons. MiraclePtr (BackupRefPtr) does not protect this path.
  • Iterator Invalidation: Erasing from the std::unordered_set only invalidates the iterator pointing to the erased element, meaning the loop iterator ‘it’ can survive and reach the end of the loop, triggering the post-loop dereference.

Potential Steps to Reproduce

Note: These are potential steps based on code analysis; our tooling does not currently have the capability to run code to confirm with a proof of concept.

  1. Load a page that triggers accelerated scrolling or CSS transformations so that global_state_.tree_priority == SMOOTHNESS_TAKES_PRIORITY is active.
  2. Force a layout/scale update on the pending tree that will cause active-tree tilings to be replaced upon activation.
  3. Arrange for two active-tree tiles (Tile A and Tile B) to be in pending_gpu_work_tiles_ with required_for_draw set to true.
  4. Trigger the completion of Tile B’s rasterization so it is ready, while Tile A remains incomplete.
  5. Wait for the signals_check_notifier_ posted task to fire CheckPendingGpuWorkAndIssueSignals.
  6. The loop visits Tile A first, caching its resource_ address in required_for_draw.
  7. The loop visits Tile B, calling NotifyTileStateChanged with set_needs_redraw=true.
  8. The scheduler synchronously runs and executes ACTIVATE_SYNC_TREE, causing the destruction of Tile A’s tiling and freeing Tile A’s memory.
  9. The loop finishes, and SetReadyToDrawCallback dereferences the dangling pointer to Tile A’s resource, causing a Use-After-Free.

Suggested Fix

To prevent re-entrancy and potential UAF, defer the redraw notifications until after the loop has completed. This is the exact pattern already applied in the sibling MarkTilesOutOfMemory loop:

// Pass set_needs_redraw = false inside the loop
if (global_state_.tree_priority != SMOOTHNESS_TAKES_PRIORITY ||
    raster_buffer_provider_->IsResourceReadyToDraw(resource)) {
  tile->draw_info().set_resource_ready_for_draw();
  client_->NotifyTileStateChanged(tile, /*update_damage=*/true,
                                  /*set_needs_redraw=*/false);
  it = pending_gpu_work_tiles_.erase(it);
  has_ready_tiles_for_redraw = true;
  continue;
}

// Trigger SetNeedsRedraw synchronously once the loop has safely finished and stack vectors are processed
if (has_ready_tiles_for_redraw) {
  client_->SetNeedsRedraw(/*animation_only=*/false, /*skip_if_inside_draw=*/true);
}

Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d


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