Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Compositing
DescriptionUse after free in Compositing
ComponentCompositing
Bug ClassUAF
Tracker501604761
Fix commit424ab5d800ff (chromium/src) +157/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
cc/tiles/tile_manager.cc
modified

Files Changed

  • cc/tiles/tile_manager.cc
From 424ab5d800ff8f1d3c2fc776f9ef00862fc54474 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Tue, 14 Jul 2026 13:20:19 -0700
Subject: [PATCH] cc: Defer SetNeedsRedraw while iterating tile priority queues

AssignGpuMemoryToTiles() and the eviction helpers it uses notify the
client of each tile's state change with set_needs_redraw=true while a
Raster/EvictionTilePriorityQueue is still being iterated. From the
posted CheckIfMoreTilesNeedToBePrepared task this may re-enter
Scheduler::ProcessScheduledActions() and trigger ActivateSyncTree(),
which can remove tilings still referenced by the live priority-queue
iterators.

Apply the same defer pattern used by MarkTilesOutOfMemory(): pass
set_needs_redraw=false for each tile, accumulate whether any
required-for-draw tile changed state, and request a single redraw once
the queues and the PrioritizedTiles derived from them have been fully
consumed. This covers the eviction loops and the solid-color path in
AssignGpuMemoryToTiles(), as well as TrimPrepaintTiles() and
ReduceTileMemoryWhenIdle() which share the same helpers.

Bug: 501604761
Change-Id: I4b78af9286a1c8f1a0cda2f30f2249c8ab6dba44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8088758
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: vikas soni <vikassoni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1662124}
---

diff --git a/cc/tiles/tile_manager.cc b/cc/tiles/tile_manager.cc
index 890ab156..27f120ed 100644
--- a/cc/tiles/tile_manager.cc
+++ b/cc/tiles/tile_manager.cc
@@ -508,8 +508,13 @@
   // Note: we don't need to flush anything here, even though this is a case
   // where frames are not being produced. The resource pool will itself issue a
   // flush after a few seconds when a resource becomes unused.
+  bool freed_required_for_draw_tile = false;
   FreeTileResourcesWithLowerPriorityUntilUsageIsWithinLimit(
-      nullptr, limit, kVisiblePriority, &usage);
+      nullptr, limit, kVisiblePriority, &usage, &freed_required_for_draw_tile);
+  if (freed_required_for_draw_tile) {
+    client_->SetNeedsRedraw(/*animation_only=*/false,
+                            /*skip_if_inside_draw=*/true);
+  }
 }
 
 void TileManager::TrimPrepaintTiles() {
@@ -518,6 +523,7 @@
   std::unique_ptr<EvictionTilePriorityQueue> eviction_priority_queue =
       client_->BuildEvictionQueue();
   bool has_eligible_used_tiles = false;
+  bool freed_required_for_draw_tile = false;
   for (; !eviction_priority_queue->IsEmpty(); eviction_priority_queue->Pop()) {
     const auto& prioritized_tile = eviction_priority_queue->Top();
     Tile* tile = prioritized_tile.tile();
@@ -546,7 +552,8 @@
       // PictureLayerTiling::ComputePriorityForTile() sets the bin to EVENTUALLY
       // regardless (because the client doesn't have valid priorities).
       // We don't want to keep these tiles, so no DCHECK() or exclusion here.
-      FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
+      freed_required_for_draw_tile |=
+          FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
     } else {
       // Tile has been used recently, reset this so that if it's not used until
       // the next reclaim task, then we know it has been at least
@@ -556,6 +563,11 @@
       has_eligible_used_tiles = true;
     }
   }
+  eviction_priority_queue.reset();
+  if (freed_required_for_draw_tile) {
+    client_->SetNeedsRedraw(/*animation_only=*/false,
+                            /*skip_if_inside_draw=*/true);
+  }
 
   // Reschedule the task, since there are tiles that would be eligible to evict
   // if they were old enough. Note that we don't choose the smallest delay
@@ -717,9 +729,22 @@
       !prioritized_work.tiles_to_raster.empty() &&
       prioritized_work.tiles_to_raster.front().tile()->required_for_draw());
 
+  const bool required_for_draw_tile_state_changed =
+      prioritized_work.required_for_draw_tile_state_changed;
+
   // Schedule tile tasks.
   ScheduleTasks(std::move(prioritized_work));
 
+  // If we trigger SetNeedsRedraw() while iterating the priority queues, 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 queues. Defer the redraw until
+  // the queues and the prioritized tiles they produced have been consumed.
+  if (required_for_draw_tile_state_changed) {
+    client_->SetNeedsRedraw(/*animation_only=*/false,
+                            /*skip_if_inside_draw=*/true);
+  }
+
   TRACE_EVENT_INSTANT("cc", "DidPrepareTiles", "state", BasicStateAsValue());
   return true;
 }
@@ -823,7 +848,8 @@
 TileManager::FreeTileResourcesUntilUsageIsWithinLimit(
     std::unique_ptr<EvictionTilePriorityQueue> eviction_priority_queue,
     const MemoryUsage& limit,
-    MemoryUsage* usage) {
+    MemoryUsage* usage,
+    bool* freed_required_for_draw_tile) {
   while (usage->Exceeds(limit)) {
     if (!eviction_priority_queue) {
       eviction_priority_queue = client_->BuildEvictionQueue();
@@ -833,7 +859,8 @@
 
     Tile* tile = eviction_priority_queue->Top().tile();
     *usage -= MemoryUsage::FromTile(tile);
-    FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
+    *freed_required_for_draw_tile |=
+        FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
     eviction_priority_queue->Pop();
   }
   return eviction_priority_queue;
@@ -844,7 +871,8 @@
     std::unique_ptr<EvictionTilePriorityQueue> eviction_priority_queue,
     const MemoryUsage& limit,
     const TilePriority& other_priority,
-    MemoryUsage* usage) {
+    MemoryUsage* usage,
+    bool* freed_required_for_draw_tile) {
   while (usage->Exceeds(limit)) {
     if (!eviction_priority_queue) {
       eviction_priority_queue = client_->BuildEvictionQueue();
@@ -858,7 +886,8 @@
 
     Tile* tile = prioritized_tile.tile();
     *usage -= MemoryUsage::FromTile(tile);
-    FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
+    *freed_required_for_draw_tile |=
+        FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(tile);
     eviction_priority_queue->Pop();
   }
   return eviction_priority_queue;
@@ -940,7 +969,9 @@
       if (is_solid_color) {
         tile->draw_info().set_solid_color(color);
         client_->NotifyTileStateChanged(tile, /*update_damage=*/true,
-                                        /*set_needs_redraw=*/true);
+                                        /*set_needs_redraw=*/false);
+        work_to_schedule.required_for_draw_tile_state_changed |=
+            tile->required_for_draw();
         continue;
       }
     }
@@ -1003,7 +1034,8 @@
     eviction_priority_queue =
         FreeTileResourcesWithLowerPriorityUntilUsageIsWithinLimit(
             std::move(eviction_priority_queue), scheduled_tile_memory_limit,
-            priority, &memory_usage);
+            priority, &memory_usage,
+            &work_to_schedule.required_for_draw_tile_state_changed);
     bool memory_usage_is_within_limit =
         !memory_usage.Exceeds(scheduled_tile_memory_limit);
 
@@ -1056,7 +1088,8 @@
   // didn't reduce memory. This ensures that we always release as many resources
   // as possible to stay within the memory limit.
   eviction_priority_queue = FreeTileResourcesUntilUsageIsWithinLimit(
-      std::move(eviction_priority_queue), hard_memory_limit, &memory_usage);
+      std::move(eviction_priority_queue), hard_memory_limit, &memory_usage,
+      &work_to_schedule.required_for_draw_tile_state_changed);
 
   // At this point, if we ran out of memory when allocating resources and we
   // couldn't go past even the NOW bin, this means we have evicted resources
@@ -1168,13 +1201,17 @@
   }
 }
 
-void TileManager::FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(
+bool TileManager::FreeResourcesForTileAndNotifyClientIfTileWasReadyToDraw(
     Tile* tile) {
   TRACE_EVENT0("viz", __PRETTY_FUNCTION__);
   bool was_ready_to_draw = tile->draw_info().IsReadyToDraw();
   FreeResourcesForTile(tile);
+  // Do not request a redraw here; this is always called while a priority queue
+  // holding raw tile/tiling pointers is being iterated. The caller is
+  // responsible for requesting a single redraw once iteration is complete.
   client_->NotifyTileStateChanged(tile, /*update_damage=*/was_ready_to_draw,
-                                  /*set_needs_redraw=*/true);
+                                  /*set_needs_redraw=*/false);
+  return tile->required_for_draw();
 }
 
 void TileManager::PartitionImagesForCheckering(
@@ -1916,13 +1953,25 @@
       !work_to_schedule.tiles_to_raster.empty() &&
       work_to_schedule.tiles_to_raster.front().tile()->required_for_draw());
 
+  const bool required_for_draw_tile_state_changed =
+      work_to_schedule.required_for_draw_tile_state_changed;
+
   // |tiles_that_need_to_be_rasterized| will be empty when we reach a
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 eba043b..b7eaf7f9 100644
--- a/cc/tiles/tile_manager_unittest.cc
+++ b/cc/tiles/tile_manager_unittest.cc
@@ -46,6 +46,7 @@
 #include "cc/test/test_task_graph_runner.h"
 #include "cc/test/test_tile_priorities.h"
 #include "cc/tiles/eviction_tile_priority_queue.h"
+#include "cc/tiles/picture_layer_tiling_set.h"
 #include "cc/tiles/raster_tile_priority_queue.h"
 #include "cc/tiles/tile.h"
 #include "cc/tiles/tile_priority.h"
@@ -1897,6 +1898,89 @@
   }
 }
 
+class TileManagerDeferredRedrawTest : public TestLayerTreeHostBase {
+ public:
+  // Host impl that simulates a client which removes tilings synchronously when
+  // notified of a tile state change with `set_needs_redraw` set. TileManager
+  // must defer SetNeedsRedraw() until it has finished iterating its priority
+  // queues so that it is safe for the client to do this.
+  class ReentrantHostImpl : public FakeLayerTreeHostImpl {
+   public:
+    using FakeLayerTreeHostImpl::FakeLayerTreeHostImpl;
+
+    void NotifyTileStateChanged(const Tile* tile,
+                                bool update_damage,
+                                bool set_needs_redraw) override {
+      if (set_needs_redraw && tilings_to_remove_) {
+        ++notify_with_redraw_count_;
+        PictureLayerTilingSet* tilings = tilings_to_remove_;
+        tilings_to_remove_ = nullptr;
+        tilings->RemoveAllTilings();
+      }
+    }
+
+    void SetNeedsRedraw(bool animation_only,
+                        bool skip_if_inside_draw) override {
+      ++set_needs_redraw_count_;
+    }
+
+    void set_tilings_to_remove(PictureLayerTilingSet* tilings) {
+      tilings_to_remove_ = tilings;
+    }
+    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;
+    int notify_with_redraw_count_ = 0;
+    int set_needs_redraw_count_ = 0;
+  };
+
+  std::unique_ptr<FakeLayerTreeHostImpl> CreateHostImpl(
+      const LayerTreeSettings& settings,
+      TaskRunnerProvider* task_runner_provider,
+      TaskGraphRunner* task_graph_runner) override {
+    return std::make_unique<ReentrantHostImpl>(settings, task_runner_provider,
+                                               task_graph_runner);
+  }
+
+  std::unique_ptr<LayerTreeFrameSink> CreateLayerTreeFrameSink() override {
+    return FakeLayerTreeFrameSink::CreateSoftware();
+  }
+
+  ReentrantHostImpl* reentrant_host_impl() {
+    return static_cast<ReentrantHostImpl*>(host_impl());
+  }
+};
+
+TEST_F(TileManagerDeferredRedrawTest, DeferRedrawWhileFreeingTileResources) {
+  const gfx::Size layer_bounds(1000, 1000);
+  host_impl()->active_tree()->SetDeviceViewportRect(gfx::Rect(layer_bounds));
+  SetupDefaultTrees(layer_bounds);
+
+  std::vector<Tile*> active_tiles =
+      active_layer()->HighResTiling()->AllTilesForTesting();
+  ASSERT_GT(active_tiles.size(), 1u);
+  host_impl()->tile_manager()->InitializeTilesWithResourcesForTesting(
+      active_tiles);
+
+  // Reduce the memory limit so that all active tiles will be evicted on the
+  // next PrepareTiles().
+  auto global_state = host_impl()->global_tile_state();
+  global_state.hard_memory_limit_in_bytes = 0u;
+  global_state.soft_memory_limit_in_bytes = 0u;
+  reentrant_host_impl()->set_tilings_to_remove(active_layer()->tilings());
+
+  // Freeing tile resources notifies the client of each tile's state change but
+  // must defer SetNeedsRedraw() until the priority queues are no longer in use.
+  // No tile state change notification should request a redraw while resources
+  // are being freed; instead a single SetNeedsRedraw() should be issued
+  // afterwards.
+  host_impl()->tile_manager()->PrepareTiles(global_state);
+  EXPECT_EQ(0, reentrant_host_impl()->notify_with_redraw_count());
+  EXPECT_LE(1, reentrant_host_impl()->set_needs_redraw_count());
+}
+
 class TileManagerOcclusionTest : public TileManagerTest {
  public:
   LayerTreeSettings CreateSettings() override {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in TileManager via synchronous tree activation during queue iteration

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: When TileManager processes tiles in a posted task, state changes can trigger synchronous Scheduler actions. This can cause the active sync tree to activate, destroying tiling objects that are currently referenced by active priority queues. Because these queues use RAW_PTR_EXCLUSION for performance, dereferencing them subsequently leads to a potential Use-After-Free.

Affected files:

  • cc/tiles/tile_manager.cc
  • cc/tiles/tiling_set_eviction_queue.h
  • cc/tiles/tiling_set_raster_queue_all.h
  • cc/tiles/prioritized_tile.h
  • cc/scheduler/scheduler.cc
  • cc/tiles/picture_layer_tiling_set.cc

Estimated timestamp from git blame: 2025-07-11

Technical Details

There is a potential Use-After-Free (UAF) vulnerability in cc::TileManager caused by re-entrant scheduler actions destroying objects that are currently being iterated over.

TileManager::CheckIfMoreTilesNeedToBePrepared is executed via more_tiles_need_prepare_check_notifier_, which is a UniqueNotifier that posts a task to the compositor thread. Because this runs as a top-level posted task rather than inside a scheduler frame cycle, the Scheduler re-entrancy guards (inside_process_scheduled_actions_ and inside_scheduled_action_) are false.

When CheckIfMoreTilesNeedToBePrepared executes, it calls AssignGpuMemoryToTiles(), which constructs RasterTilePriorityQueue and EvictionTilePriorityQueue objects to manage memory. These queues iterate over PictureLayerTiling and Tile objects. For performance reasons, the internal iterators of these queues (e.g., TilingIterator in cc/tiles/tiling_set_raster_queue_all.h) store raw pointers to the tilings explicitly marked with RAW_PTR_EXCLUSION, bypassing MiraclePtr protections.

During iteration in AssignGpuMemoryToTiles() (or its eviction helpers), the code may determine that a tile is a solid color, or that it needs to be evicted. When this happens, it calls client_->NotifyTileStateChanged(..., set_needs_redraw=true).

The set_needs_redraw=true parameter propagates through LayerTreeHostImpl and ProxyImpl to Scheduler::SetNeedsRedraw(), which synchronously executes ProcessScheduledActions(). Because the re-entrancy guards are currently false, the state machine evaluates available actions. If the pending tree is ready, it may select Action::ACTIVATE_SYNC_TREE.

Executing ACTIVATE_SYNC_TREE causes PictureLayerTilingSet::UpdateTilingsToCurrentRasterSourceForActivation to synchronize tilings, which often results in the deletion of the old PictureLayerTiling objects and their owned Tile objects. When the call stack unwinds back to the loop in AssignGpuMemoryToTiles(), the loop continues and calls raster_priority_queue->Pop(). This method attempts to advance the iterator by dereferencing the RAW_PTR_EXCLUSION pointer (tiling_->TileAt(...)), resulting in a Use-After-Free on the destroyed PictureLayerTiling.

Interestingly, this hazard is known in a different part of the file. In TileManager::MarkTilesOutOfMemory, a comment explicitly notes: “If we trigger SetNeedsRedraw() inside the loop above, we may end up triggering Scheduler::ProcessScheduledActions()… Worth, it may in turn trigger ActivateSyncTree() and other actions that remove tiles in the queue, leading to UAF.” However, the protection implemented there (deferring SetNeedsRedraw) was not applied to AssignGpuMemoryToTiles.

Potential Attacker Steps

Note: These are suggested/potential steps, as our tooling agent does not yet have the ability to run code or build a proof-of-concept.

  1. Trigger Layout/Paint Operations: The attacker creates a webpage that heavily manipulates layers (e.g., via CSS will-change: transform, animations, or inserting many solid-colored <div> elements) to push the compositor to its limits and prepare a pending tree for activation.
  2. Force Task Execution: The page activity causes more_tiles_need_prepare_check_notifier_ to be scheduled, kicking off AssignGpuMemoryToTiles in a fresh posted task.
  3. Trigger State Change: The iteration hits a solid color tile, triggering NotifyTileStateChanged and synchronously activating the sync tree, freeing the currently iterated PictureLayerTiling objects.
  4. Heap Grooming: Prior to this cycle, the attacker uses WebAssembly or JavaScript ArrayBuffers to manipulate the PartitionAlloc heap, preparing fake PictureLayerTiling or Tile objects to reclaim the freed memory.
  5. Hijack Execution: When queue->Pop() dereferences the dangling pointer, it accesses the attacker’s fake object. By providing a fake virtual table for internal task references (e.g., raster_task_), the attacker redirects execution when RunOnWorkerThread() is invoked, potentially achieving Remote Code Execution (RCE) in the renderer process.

Suggested Fix

Similar to the strategy employed in TileManager::MarkTilesOutOfMemory, TileManager::AssignGpuMemoryToTiles and its eviction helpers should defer calling SetNeedsRedraw until after the priority queues have finished processing.

Modify client_->NotifyTileStateChanged calls inside these loops to pass set_needs_redraw=false. Track a boolean flag needs_redraw during the queue iterations. Once all priority queues have been emptied and the loops have completed, call client_->SetNeedsRedraw(/*animation_only=*/false, /*skip_if_inside_draw=*/true) if the flag was set.

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