CVE-2026-7357
Overview
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_maponCompositorFrameSinkSupportkeyed byViewTransitionToken, whose contiguous backing storage is reallocated (invalidating iterators) when entries are inserted. - View-transition save directive
- a
CompositorFrameTransitionDirectiveof the “save” kind, processed byOnSaveTransitionDirectiveProcessed, which can hand its animation manager toFrameSinkManagerImpl::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.
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
- Establish a saved transition
A page initiates a cross-frame-sink view transition so that a
SurfaceAnimationManageris registered under a token inview_transition_token_to_animation_manager_. - 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.
- 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 invalidatingit. - Use the dangling iterator
Control returns to
OnSaveTransitionDirectiveProcessed, which executeserase(it)on the stale iterator, dereferencing freedbase::flat_mapstorage (the use-after-free the regression test provokes). - 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
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
| Function | Change | Notes |
|---|---|---|
forcomponents/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc |
modified |
Files Changed
components/viz/service/frame_sinks/compositor_frame_sink_support.cccomponents/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cc
Audit Directions
- Iterator held across a callbackFlag 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 mutationAudit 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 vizSearch
components/viz/service/frame_sinksforerase(it)following a non-trivial function call and verify the container cannot be mutated in between.
Patch
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
Regression Test / PoC
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
Original Bug Report
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.cccomponents/viz/service/frame_sinks/compositor_frame_sink_support.hcomponents/viz/service/surfaces/surface.cccomponents/viz/service/frame_sinks/frame_sink_manager_impl.cccomponents/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:
CacheSurfaceAnimationManagersynchronously notifies observers viaOnViewTransitionSaved.- An observing
Surfacemay have its view transition dependencies resolved by this notification, prompting it to immediately callActivatePendingFrame(). ActivatePendingFrame()synchronously calls back intoCompositorFrameSinkSupport::OnSurfaceActivated(this).OnSurfaceActivatedprocesses 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.
- From a compromised renderer, submit Frame 1 with a
kSavedirective forTokenX(delay_layer_tree_view_deletion=true,maybe_cross_frame_sink=true). This schedulesOnSaveTransitionDirectiveProcessedto run via a posted task. - Immediately submit Frame 2 with a
kAnimateRendererdirective forTokenXand a large number ofkSavedirectives. Frame 2 will pend waiting forTokenXto be cached. - Allow the posted task to run. It acquires the iterator and calls
CacheSurfaceAnimationManager. - Frame 2 is synchronously unblocked and activated. Its directives are processed, reallocating the map and freeing the old buffer.
- 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.