High watch ✔︎ verified CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Sub-componentViz
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Mechanism matches diff. 'default' preconditions overstated: the claim itself requires shaping frame timing, and the regression test needs a pending cross-frame-sink frame carrying ~100 new save directives to force map reallocation.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker497047552
Fix commit41369deb367d (chromium/src) +79/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-27 (per vendor advisory)
Fix landed2026-04-07 (public git)
Disclosed2026-04-28
Patch gap21 days — fix public in source before the advisory

Background

`CompositorFrameSinkSupport`
the viz-service object that receives CompositorFrames and transition directives from a client and drives their processing in the GPU process.
`view_transition_token_to_animation_manager_`
a base::flat_map on CompositorFrameSinkSupport keyed by ViewTransitionToken, whose contiguous backing storage is reallocated (invalidating iterators) when entries are inserted.
View-transition save directive
a CompositorFrameTransitionDirective of the “save” kind, processed by OnSaveTransitionDirectiveProcessed, which can hand its animation manager to FrameSinkManagerImpl::CacheSurfaceAnimationManager.
Re-entrancy
a nested, synchronous call back into the same subsystem triggered while a function is still executing, here caused by surface activation notifying observers mid-directive-processing.

Root Cause Analysis

In OnSaveTransitionDirectiveProcessed, the code held an iterator it into view_transition_token_to_animation_manager_ and, for a cross-frame-sink directive, called frame_sink_manager_->CacheSurfaceAnimationManager(...) while still intending to erase(it) afterward. The call to CacheSurfaceAnimationManager can synchronously trigger surface activation, which notifies FrameSinkObservers; that notification can cause new SurfaceAnimationManager entries to be inserted into the very same base::flat_map, forcing it to reallocate and invalidating it. The subsequent view_transition_token_to_animation_manager_.erase(it) then operated on a dangling iterator, a use-after-free of the map’s freed backing storage. The invariant violated is that a base::flat_map iterator stays valid only if the container is not mutated between acquisition and use.

The fix re-looks-up the element by key, replacing erase(it) with erase(directive.transition_token()), so the erase resolves the element afresh after the re-entrant mutation and never touches a stale iterator.

Key insight
The single mistake was reusing a base::flat_map iterator across a call (CacheSurfaceAnimationManager) that can re-entrantly insert into that same map; the fix erases by the stable directive.transition_token() key instead of the invalidated iterator.

Attack Path

  1. Establish a saved transition A page initiates a cross-frame-sink view transition so that a SurfaceAnimationManager is registered under a token in view_transition_token_to_animation_manager_.
  2. Queue a dependent, transition-heavy frame The page submits a later frame that waits on the first transition and itself requests many new save transitions, so that when it activates it will insert numerous new map entries.
  3. Trigger re-entrant activation Completion of the first transition is signaled; processing it calls CacheSurfaceAnimationManager, whose synchronous surface activation notifies observers that insert the pending frame’s new managers, reallocating the map and invalidating it.
  4. Use the dangling iterator Control returns to OnSaveTransitionDirectiveProcessed, which executes erase(it) on the stale iterator, dereferencing freed base::flat_map storage (the use-after-free the regression test provokes).
  5. Exploitation Controlling the layout and timing of the freed/reallocated storage could turn the UAF into a corruption primitive in the GPU process (inferred); the patch itself only demonstrates the crash.

Impact Assessment

An attacker controlling a renderer/web content can drive the viz compositor in the GPU process into a use-after-free on a base::flat_map’s backing store, yielding at minimum a GPU-process crash and potentially a memory-corruption primitive there (inferred). The vendor advisory rates this high severity (use after free in GPU); it reports no exploited-in-the-wild status, and any escalation beyond the demonstrated UAF is inferred. Preconditions are the ability to script view transitions and shape frame timing so activation re-enters during save-directive processing.

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

Audit Directions

  • Iterator held across a callback
    Flag any base::flat_map/std::vector-backed iterator or reference retained across a call that can run client-observer or activation callbacks; re-resolve by key after such calls.
  • Re-entrant container mutation
    Audit view-transition and surface-activation paths where notifying FrameSinkObservers can synchronously insert into maps that the caller is mid-iteration over.
  • Erase-by-iterator patterns in viz
    Search components/viz/service/frame_sinks for erase(it) following a non-trivial function call and verify the container cannot be mutated in between.
From 41369deb367db53689068afcabf4523499215732 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <[email protected]>
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 <[email protected]>
Commit-Queue: Arthur Sonzogni <[email protected]>
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 [email protected]

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