Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Tracker497047552
Fix commit41369deb367d (chromium/src) +79/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
for
components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
modified

Files Changed

  • components/viz/service/frame_sinks/compositor_frame_sink_support.cc
  • components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
From 41369deb367db53689068afcabf4523499215732 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <arthursonzogni@chromium.org>
Date: Tue, 07 Apr 2026 02:20:42 -0700
Subject: [PATCH] [viz] Fix UAF in OnSaveTransitionDirectiveProcessed due to re-entry

A Use-After-Free (UAF) was identified in
CompositorFrameSinkSupport::OnSaveTransitionDirectiveProcessed.
The issue is caused by the invalidation of a base::flat_map iterator
during a synchronous, re-entrant frame activation sequence.

When CacheSurfaceAnimationManager is called, it can synchronously
trigger surface activation, which in turn can modify the
view_transition_token_to_animation_manager_ map, causing it to
reallocate and invalidate any existing iterators.

This CL fixes the issue by avoiding the use of the iterator after
the call to CacheSurfaceAnimationManager, instead using the token
key to erase the element from the map.

A regression test is added to compositor_frame_sink_support_unittest.cc
which simulates a re-entrant frame activation that causes map
reallocation during OnSaveTransitionDirectiveProcessed.

Fixed: 497047552
Test: CompositorFrameSinkSupportTestBase.OnSaveTransitionDirectiveProcessedReentryUAF
Change-Id: I52d69bfbfb619b25baf3be81d522714e378b3eee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7717958
Reviewed-by: Vladimir Levin <vmpstr@chromium.org>
Commit-Queue: Arthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1610628}
---

diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
index 5ef7760..b0a9ae6 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
@@ -1717,10 +1717,15 @@
         directive.sequence_id());
   }
 
+  // Subtle: the iterator `it` may be invalidated after the call to
+  // `CacheSurfaceAnimationManager` due to new SurfaceAnimationManager being
+  // created and put into the map. This can happen due to the FrameSinkObserver
+  // getting notified of the view transition saving surface being activated.
   if (directive.maybe_cross_frame_sink()) {
     frame_sink_manager_->CacheSurfaceAnimationManager(
         directive.transition_token(), std::move(it->second));
-    view_transition_token_to_animation_manager_.erase(it);
+    view_transition_token_to_animation_manager_.erase(
+        directive.transition_token());
   }
 }
 
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc b/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
index 59ce418f..f0dff13 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
@@ -34,6 +34,7 @@
 #include "components/viz/common/surfaces/surface_info.h"
 #include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
 #include "components/viz/service/surfaces/surface.h"
+#include "components/viz/service/transitions/surface_animation_manager.h"
 #include "components/viz/test/begin_frame_args_test.h"
 #include "components/viz/test/compositor_frame_helpers.h"
 #include "components/viz/test/fake_compositor_frame_sink_client.h"
@@ -298,6 +299,12 @@
     return !support->view_transition_token_to_animation_manager_.empty();
   }
 
+  void OnSaveTransitionDirectiveProcessed(
+      CompositorFrameSinkSupport* support,
+      const CompositorFrameTransitionDirective& directive) {
+    support->OnSaveTransitionDirectiveProcessed(directive);
+  }
+
  protected:
   TestSharedImageInterfaceProvider shared_image_interface_provider_;
   std::unique_ptr<base::SimpleTestTickClock> now_src_;
@@ -2727,4 +2734,70 @@
             kVideoInterval);
 }
 
+// Regression test for https://crbug.com/497047552.
+TEST_F(CompositorFrameSinkSupportTestBase,
+       OnSaveTransitionDirectiveProcessedReentryUAF) {
+  // This test ensures we don't crash when processing a transition completion
+  // that triggers a chain reaction of new transition requests.
+  //
+  // 1. We set up an initial transition.
+  blink::ViewTransitionToken token_x;
+
+  // Create Surface A by submitting a frame.
+  SubmitCompositorFrameWithResources({});
+  Surface* surface_a = support_->GetLastCreatedSurfaceForTesting();
+  ASSERT_TRUE(surface_a);
+
+  // 2. We submit a request to save the transition for our token.
+  auto save_directive = CompositorFrameTransitionDirective::CreateSave(
+      token_x, /*maybe_cross_frame_sink=*/true, 1, {}, {}, false);
+  ProcessCompositorFrameTransitionDirective(support_.get(), save_directive,
+                                            surface_a);
+  ASSERT_TRUE(SupportHasSurfaceAnimationManager(support_.get()));
+
+  // 3. We prepare a second frame that's waiting on this transition. This
+  // frame is special because it also asks for many more new transitions.
+  LocalSurfaceId local_surface_id_b(
+      local_surface_id_.parent_sequence_number() + 1,
+      local_surface_id_.embed_token());
+
+  CompositorFrame frame_2 =
+      MakeDefaultInteractiveCompositorFrame(kBeginFrameSourceId);
+  // Add the requirement that token_x must be finished before this frame can
+  // be displayed.
+  frame_2.metadata.transition_directives.push_back(
+      CompositorFrameTransitionDirective::CreateAnimate(token_x, true, 2,
+                                                        true));
+
+  // Add MANY more new transition requests. This will force the system to
+  // reorganize its internal storage when they are processed.
+  for (int i = 0; i < 100; ++i) {
+    frame_2.metadata.transition_directives.push_back(
+        CompositorFrameTransitionDirective::CreateSave(
+            blink::ViewTransitionToken(), true, 100 + i, {}, {}, false));
+  }
+
+  // Submit the second frame. It will stay "pending" because it is waiting
+  // for the first transition to complete.
+  support_->SubmitCompositorFrame(local_surface_id_b, std::move(frame_2));
+
+  SurfaceId surface_id_b(support_->frame_sink_id(), local_surface_id_b);
+  Surface* surface_b =
+      manager_->surface_manager()->GetSurfaceForId(surface_id_b);
+  ASSERT_TRUE(surface_b);
+  ASSERT_TRUE(surface_b->HasPendingFrame());
+
+  // 4. We now signal that the first transition is complete.
+  // This triggers a chain reaction:
+  // - The system marks the first transition as finished.
+  // - This allows the second frame to finally become active.
+  // - As the second frame becomes active, it registers all its many new
+  //   transition requests.
+  // - These new requests cause the internal storage to be reallocated,
+  //   invalidating current iterators.
+  // - Finally, we finish the cleanup for the original transition. If we were
+  //   still using an outdated reference to the storage, we would crash here.
+  OnSaveTransitionDirectiveProcessed(support_.get(), save_directive);
+}
+
 }  // namespace viz
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc b/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
index 59ce418f..f0dff13 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
@@ -34,6 +34,7 @@
 #include "components/viz/common/surfaces/surface_info.h"
 #include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
 #include "components/viz/service/surfaces/surface.h"
+#include "components/viz/service/transitions/surface_animation_manager.h"
 #include "components/viz/test/begin_frame_args_test.h"
 #include "components/viz/test/compositor_frame_helpers.h"
 #include "components/viz/test/fake_compositor_frame_sink_client.h"
@@ -298,6 +299,12 @@
     return !support->view_transition_token_to_animation_manager_.empty();
   }
 
+  void OnSaveTransitionDirectiveProcessed(
+      CompositorFrameSinkSupport* support,
+      const CompositorFrameTransitionDirective& directive) {
+    support->OnSaveTransitionDirectiveProcessed(directive);
+  }
+
  protected:
   TestSharedImageInterfaceProvider shared_image_interface_provider_;
   std::unique_ptr<base::SimpleTestTickClock> now_src_;
@@ -2727,4 +2734,70 @@
             kVideoInterval);
 }
 
+// Regression test for https://crbug.com/497047552.
+TEST_F(CompositorFrameSinkSupportTestBase,
+       OnSaveTransitionDirectiveProcessedReentryUAF) {
+  // This test ensures we don't crash when processing a transition completion
+  // that triggers a chain reaction of new transition requests.
+  //
+  // 1. We set up an initial transition.
+  blink::ViewTransitionToken token_x;
+
+  // Create Surface A by submitting a frame.
+  SubmitCompositorFrameWithResources({});
+  Surface* surface_a = support_->GetLastCreatedSurfaceForTesting();
+  ASSERT_TRUE(surface_a);
+
+  // 2. We submit a request to save the transition for our token.
+  auto save_directive = CompositorFrameTransitionDirective::CreateSave(
+      token_x, /*maybe_cross_frame_sink=*/true, 1, {}, {}, false);
+  ProcessCompositorFrameTransitionDirective(support_.get(), save_directive,
+                                            surface_a);
+  ASSERT_TRUE(SupportHasSurfaceAnimationManager(support_.get()));
+
+  // 3. We prepare a second frame that's waiting on this transition. This
+  // frame is special because it also asks for many more new transitions.
+  LocalSurfaceId local_surface_id_b(
+      local_surface_id_.parent_sequence_number() + 1,
+      local_surface_id_.embed_token());
+
+  CompositorFrame frame_2 =
+      MakeDefaultInteractiveCompositorFrame(kBeginFrameSourceId);
+  // Add the requirement that token_x must be finished before this frame can
+  // be displayed.
+  frame_2.metadata.transition_directives.push_back(
+      CompositorFrameTransitionDirective::CreateAnimate(token_x, true, 2,
+                                                        true));
+
+  // Add MANY more new transition requests. This will force the system to
+  // reorganize its internal storage when they are processed.
+  for (int i = 0; i < 100; ++i) {
+    frame_2.metadata.transition_directives.push_back(
+        CompositorFrameTransitionDirective::CreateSave(
+            blink::ViewTransitionToken(), true, 100 + i, {}, {}, false));
+  }
+
+  // Submit the second frame. It will stay "pending" because it is waiting
+  // for the first transition to complete.
+  support_->SubmitCompositorFrame(local_surface_id_b, std::move(frame_2));
+
+  SurfaceId surface_id_b(support_->frame_sink_id(), local_surface_id_b);
+  Surface* surface_b =
+      manager_->surface_manager()->GetSurfaceForId(surface_id_b);
+  ASSERT_TRUE(surface_b);
+  ASSERT_TRUE(surface_b->HasPendingFrame());
+
+  // 4. We now signal that the first transition is complete.
+  // This triggers a chain reaction:
+  // - The system marks the first transition as finished.
+  // - This allows the second frame to finally become active.
+  // - As the second frame becomes active, it registers all its many new
+  //   transition requests.
+  // - These new requests cause the internal storage to be reallocated,
+  //   invalidating current iterators.
+  // - Finally, we finish the cleanup for the original transition. If we were
+  //   still using an outdated reference to the storage, we would crash here.
+  OnSaveTransitionDirectiveProcessed(support_.get(), save_directive);
+}
+
 }  // namespace viz
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF via flat_map iterator invalidation in CompositorFrameSinkSupport

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A Use-After-Free vulnerability exists in CompositorFrameSinkSupport::OnSaveTransitionDirectiveProcessed when a base::flat_map iterator is invalidated during a synchronous re-entrant callback. An attacker-controlled renderer can trigger frame activation that reallocates the map’s underlying vector, leaving a dangling iterator.

Affected files:

  • components/viz/service/frame_sinks/compositor_frame_sink_support.cc
  • components/viz/service/frame_sinks/compositor_frame_sink_support.h
  • components/viz/service/surfaces/surface.cc
  • components/viz/service/frame_sinks/frame_sink_manager_impl.cc
  • components/viz/service/surfaces/surface_saved_frame.cc

Estimated timestamp from git blame: 2024-04-30

Summary

A potential Use-After-Free (UAF) and heap corruption vulnerability exists in CompositorFrameSinkSupport::OnSaveTransitionDirectiveProcessed. The issue is caused by the invalidation of a base::flat_map iterator during a synchronous, re-entrant frame activation sequence.

Vulnerability Details

The view_transition_token_to_animation_manager_ field is a base::flat_map, which is backed by a contiguous std::vector. In OnSaveTransitionDirectiveProcessed, an iterator it is acquired for a given token. If maybe_cross_frame_sink is true, the code calls frame_sink_manager_->CacheSurfaceAnimationManager(), passing the token.

This call can synchronously trigger a complex re-entrant chain:

  1. CacheSurfaceAnimationManager synchronously notifies observers via OnViewTransitionSaved.
  2. An observing Surface may have its view transition dependencies resolved by this notification, prompting it to immediately call ActivatePendingFrame().
  3. ActivatePendingFrame() synchronously calls back into CompositorFrameSinkSupport::OnSurfaceActivated(this).
  4. OnSurfaceActivated processes the newly activated frame’s directives.

If an attacker in a compromised renderer carefully queues a pending frame (Frame 2) containing numerous kSave directives, this re-entrant processing will insert many new elements into view_transition_token_to_animation_manager_. This exceeds the underlying std::vector’s capacity, forcing a reallocation. The old backing buffer—which it points to—is freed.

When the call stack unwinds back to OnSaveTransitionDirectiveProcessed, the code executes view_transition_token_to_animation_manager_.erase(it) using the dangling iterator.

Under libc++, std::vector::erase(position) calculates the element index via position - cbegin(). Because position points to the old freed buffer and cbegin() points to the new buffer, this pointer arithmetic ((old_ptr - new_ptr) / sizeof(element)) yields a wildly out-of-bounds index. The subsequent std::move operation will perform a massive out-of-bounds memory read/write across the GPU process heap.

Potential Impact

This will reliably cause a severe crash (Denial of Service) in the GPU process when the out-of-bounds std::move hits unmapped guard pages. If an attacker can perfectly groom the heap to control the distance between the old and new allocations, it could theoretically be leveraged for GPU process memory corruption and sandbox escape, though this is practically difficult.

Suggested Reproduction Steps

Note: Our tooling agent cannot run code yet, so these are theoretical steps.

  1. From a compromised renderer, submit Frame 1 with a kSave directive for TokenX (delay_layer_tree_view_deletion=true, maybe_cross_frame_sink=true). This schedules OnSaveTransitionDirectiveProcessed to run via a posted task.
  2. Immediately submit Frame 2 with a kAnimateRenderer directive for TokenX and a large number of kSave directives. Frame 2 will pend waiting for TokenX to be cached.
  3. Allow the posted task to run. It acquires the iterator and calls CacheSurfaceAnimationManager.
  4. Frame 2 is synchronously unblocked and activated. Its directives are processed, reallocating the map and freeing the old buffer.
  5. The task resumes and calls erase(it), triggering the wild pointer arithmetic and crash.

Suggested Fix

Do not use the saved iterator it after calling CacheSurfaceAnimationManager. Instead, use the token key to erase the element, which avoids iterator invalidation issues:

frame_sink_manager_->CacheSurfaceAnimationManager(
    directive.transition_token(), std::move(it->second));
view_transition_token_to_animation_manager_.erase(directive.transition_token());

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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