Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Tracker493955227
Fix commit2332137922bb (chromium/src) +67/-2
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
for
components/viz/service/surfaces/surface_manager.cc
modified
TEST_F
components/viz/service/surfaces/surface_unittest.cc
modified
for
components/viz/service/surfaces/surface_unittest.cc
modified

Files Changed

  • components/viz/service/surfaces/surface_manager.cc
  • components/viz/service/surfaces/surface_unittest.cc
From 2332137922bb5abd4ffe7ea8ae125c5f0b1d400b Mon Sep 17 00:00:00 2001
From: kylechar <kylechar@chromium.org>
Date: Fri, 10 Apr 2026 09:33:51 -0700
Subject: [PATCH] Guard against reentrant vector modification

Copy active surface group vector during before iterating as
WillNotRegisterNewSurfaces() can cause new surface groups to be added to
vector, invalidating existing iterators.

Fixed: 493955227
Change-Id: Ia58a539523d7b5d700cbfd232f4d8bab597d20d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7707244
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Commit-Queue: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1612925}
---

diff --git a/components/viz/service/surfaces/surface_manager.cc b/components/viz/service/surfaces/surface_manager.cc
index 61538500..3dafe151 100644
--- a/components/viz/service/surfaces/surface_manager.cc
+++ b/components/viz/service/surfaces/surface_manager.cc
@@ -167,7 +167,9 @@
 void SurfaceManager::InvalidateFrameSinkId(const FrameSinkId& frame_sink_id) {
   auto it = frame_sink_id_to_allocation_groups_.find(frame_sink_id);
   if (it != frame_sink_id_to_allocation_groups_.end()) {
-    for (SurfaceAllocationGroup* group : it->second) {
+    // Copy allocation group vector since it can be modified while iterating.
+    auto allocation_groups = it->second;
+    for (SurfaceAllocationGroup* group : allocation_groups) {
       group->WillNotRegisterNewSurfaces();
     }
   }
diff --git a/components/viz/service/surfaces/surface_unittest.cc b/components/viz/service/surfaces/surface_unittest.cc
index 0a5936c..aa6198f 100644
--- a/components/viz/service/surfaces/surface_unittest.cc
+++ b/components/viz/service/surfaces/surface_unittest.cc
@@ -2,12 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "components/viz/service/surfaces/surface.h"
+
 #include <utility>
 
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
+#include "base/unguessable_token.h"
 #include "cc/test/scheduler_test_common.h"
 #include "components/viz/common/features.h"
 #include "components/viz/common/frame_sinks/copy_output_result.h"
@@ -16,7 +19,6 @@
 #include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
 #include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
 #include "components/viz/service/surfaces/pending_copy_output_request.h"
-#include "components/viz/service/surfaces/surface.h"
 #include "components/viz/test/begin_frame_args_test.h"
 #include "components/viz/test/compositor_frame_helpers.h"
 #include "components/viz/test/fake_external_begin_frame_source.h"
@@ -398,5 +400,66 @@
   EXPECT_TRUE(surface->activation_dependencies().empty());
 }
 
+// Checks that modifying surface activation group vector while iterating through
+// the existing entries doesn't cause problems.
+TEST_F(SurfaceTest, RentrantSurfaceActivationGroups) {
+  SurfaceManager* surface_manager = frame_sink_manager_.surface_manager();
+
+  auto will_invalidate_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, kArbitraryFrameSinkId, /*is_root=*/false);
+  auto y1_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, FrameSinkId(3, 1), /*is_root=*/false);
+  auto y2_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, FrameSinkId(4, 1), /*is_root=*/false);
+
+  // Builds a frame with dependencies and a long deadline for activation.
+  auto build_frame = [](std::vector<SurfaceId> deps,
+                        std::vector<SurfaceRange> refs) {
+    return CompositorFrameBuilder()
+        .AddRenderPass(gfx::Rect(10, 10), gfx::Rect(10, 10))
+        .SetActivationDependencies(std::move(deps))
+        .SetReferencedSurfaces(std::move(refs))
+        .SetDeadline(FrameDeadline(base::TimeTicks::Now(), 10000u,
+                                   base::Milliseconds(16), false))
+        .Build();
+  };
+
+  // Each of these SurfaceIds are from the same FrameSinkId but have different
+  // embed_tokens therefore different SurfaceAllocationGroups.
+  std::vector<SurfaceRange> malicious_refs;
+  malicious_refs.reserve(100);
+  for (int i = 0; i < 100; i++) {
+    SurfaceId sid(kArbitraryFrameSinkId,
+                  LocalSurfaceId(i, 1, base::UnguessableToken::Create()));
+    malicious_refs.emplace_back(sid);
+  }
+
+  // A SurfaceAllocationGroup for `dep1` is added immediately but groups for
+  // `malicious_refs` are only added once the CompositorFrame activates.
+  SurfaceId dep1(kArbitraryFrameSinkId,
+                 LocalSurfaceId(1, 1, base::UnguessableToken::Create()));
+  LocalSurfaceId y1_lsid(1, 1, base::UnguessableToken::Create());
+  y1_support->SubmitCompositorFrame(y1_lsid,
+                                    build_frame({dep1}, malicious_refs));
+
+  SurfaceId dep2(kArbitraryFrameSinkId,
+                 LocalSurfaceId(2, 1, base::UnguessableToken::Create()));
+  LocalSurfaceId y2_lsid(1, 1, base::UnguessableToken::Create());
+  y2_support->SubmitCompositorFrame(y2_lsid, build_frame({dep2}, {}));
+
+  // There will be two SurfaceAllocationGroups for `kArbitraryFrameSinkId` at
+  // this point. WillNotRegisterNewSurfaces() will be called on each allocation
+  // groups, first for `y1_lsid` group which activates the surface and add 100
+  // more SurfaceAllocationGroups to the vector. This tests that modifying
+  // the vector being iterated doesn't cause problems.
+  surface_manager->InvalidateFrameSinkId(kArbitraryFrameSinkId);
+
+  // Both y1 and y2 surfaces are now active.
+  EXPECT_TRUE(surface_manager->GetSurfaceForId(
+      SurfaceId(y1_support->frame_sink_id(), y1_lsid)));
+  EXPECT_TRUE(surface_manager->GetSurfaceForId(
+      SurfaceId(y2_support->frame_sink_id(), y2_lsid)));
+}
+
 }  // namespace
 }  // namespace viz
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/viz/service/surfaces/surface_unittest.cc b/components/viz/service/surfaces/surface_unittest.cc
index 0a5936c..aa6198f 100644
--- a/components/viz/service/surfaces/surface_unittest.cc
+++ b/components/viz/service/surfaces/surface_unittest.cc
@@ -2,12 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "components/viz/service/surfaces/surface.h"
+
 #include <utility>
 
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
+#include "base/unguessable_token.h"
 #include "cc/test/scheduler_test_common.h"
 #include "components/viz/common/features.h"
 #include "components/viz/common/frame_sinks/copy_output_result.h"
@@ -16,7 +19,6 @@
 #include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
 #include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
 #include "components/viz/service/surfaces/pending_copy_output_request.h"
-#include "components/viz/service/surfaces/surface.h"
 #include "components/viz/test/begin_frame_args_test.h"
 #include "components/viz/test/compositor_frame_helpers.h"
 #include "components/viz/test/fake_external_begin_frame_source.h"
@@ -398,5 +400,66 @@
   EXPECT_TRUE(surface->activation_dependencies().empty());
 }
 
+// Checks that modifying surface activation group vector while iterating through
+// the existing entries doesn't cause problems.
+TEST_F(SurfaceTest, RentrantSurfaceActivationGroups) {
+  SurfaceManager* surface_manager = frame_sink_manager_.surface_manager();
+
+  auto will_invalidate_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, kArbitraryFrameSinkId, /*is_root=*/false);
+  auto y1_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, FrameSinkId(3, 1), /*is_root=*/false);
+  auto y2_support = std::make_unique<CompositorFrameSinkSupport>(
+      nullptr, &frame_sink_manager_, FrameSinkId(4, 1), /*is_root=*/false);
+
+  // Builds a frame with dependencies and a long deadline for activation.
+  auto build_frame = [](std::vector<SurfaceId> deps,
+                        std::vector<SurfaceRange> refs) {
+    return CompositorFrameBuilder()
+        .AddRenderPass(gfx::Rect(10, 10), gfx::Rect(10, 10))
+        .SetActivationDependencies(std::move(deps))
+        .SetReferencedSurfaces(std::move(refs))
+        .SetDeadline(FrameDeadline(base::TimeTicks::Now(), 10000u,
+                                   base::Milliseconds(16), false))
+        .Build();
+  };
+
+  // Each of these SurfaceIds are from the same FrameSinkId but have different
+  // embed_tokens therefore different SurfaceAllocationGroups.
+  std::vector<SurfaceRange> malicious_refs;
+  malicious_refs.reserve(100);
+  for (int i = 0; i < 100; i++) {
+    SurfaceId sid(kArbitraryFrameSinkId,
+                  LocalSurfaceId(i, 1, base::UnguessableToken::Create()));
+    malicious_refs.emplace_back(sid);
+  }
+
+  // A SurfaceAllocationGroup for `dep1` is added immediately but groups for
+  // `malicious_refs` are only added once the CompositorFrame activates.
+  SurfaceId dep1(kArbitraryFrameSinkId,
+                 LocalSurfaceId(1, 1, base::UnguessableToken::Create()));
+  LocalSurfaceId y1_lsid(1, 1, base::UnguessableToken::Create());
+  y1_support->SubmitCompositorFrame(y1_lsid,
+                                    build_frame({dep1}, malicious_refs));
+
+  SurfaceId dep2(kArbitraryFrameSinkId,
+                 LocalSurfaceId(2, 1, base::UnguessableToken::Create()));
+  LocalSurfaceId y2_lsid(1, 1, base::UnguessableToken::Create());
+  y2_support->SubmitCompositorFrame(y2_lsid, build_frame({dep2}, {}));
+
+  // There will be two SurfaceAllocationGroups for `kArbitraryFrameSinkId` at
+  // this point. WillNotRegisterNewSurfaces() will be called on each allocation
+  // groups, first for `y1_lsid` group which activates the surface and add 100
+  // more SurfaceAllocationGroups to the vector. This tests that modifying
+  // the vector being iterated doesn't cause problems.
+  surface_manager->InvalidateFrameSinkId(kArbitraryFrameSinkId);
+
+  // Both y1 and y2 surfaces are now active.
+  EXPECT_TRUE(surface_manager->GetSurfaceForId(
+      SurfaceId(y1_support->frame_sink_id(), y1_lsid)));
+  EXPECT_TRUE(surface_manager->GetSurfaceForId(
+      SurfaceId(y2_support->frame_sink_id(), y2_lsid)));
+}
+
 }  // namespace
 }  // namespace viz
Loading diff…

Original Bug Report

reported by je...@gmail.com

Reentrant vector mutation during FrameSink invalidation causes heap use-after-free in Viz process

Reentrant vector mutation during FrameSink invalidation causes heap use-after-free in Viz process

Summary

A heap use-after-free vulnerability exists in the Viz compositor process on all desktop platforms (Linux, macOS, Windows, ChromeOS). SurfaceManager::InvalidateFrameSinkId() iterates a vector of SurfaceAllocationGroup pointers using a range-for loop. During iteration, the synchronous callback chain through WillNotRegisterNewSurfaces can activate a pending surface whose referenced_surfaces metadata triggers creation of new allocation groups via push_back into the same vector. When the vector reallocates its internal buffer, the range-for loop’s captured iterators become dangling, and subsequent iterations read freed heap memory. A compromised renderer can construct the necessary state entirely through the EmbeddedFrameSinkProvider and CompositorFrameSink Mojo interfaces and trigger the invalidation by closing a connection. The crash occurs on the VizCompositorThread, which runs outside the renderer sandbox. ASAN confirms the region is not protected by MiraclePtr.

Bisect

Introducing Commit: 9ef8b6ebe397e7a492210006d949423ba27f5acd

Root Cause

SurfaceManager::InvalidateFrameSinkId looks up all SurfaceAllocationGroup entries associated with a given FrameSinkId and notifies each that no further surfaces will be registered:

// components/viz/service/surfaces/surface_manager.cc:166-174
void SurfaceManager::InvalidateFrameSinkId(const FrameSinkId& frame_sink_id) {
  auto it = frame_sink_id_to_allocation_groups_.find(frame_sink_id);
  if (it != frame_sink_id_to_allocation_groups_.end()) {
    for (SurfaceAllocationGroup* group : it->second) {
      group->WillNotRegisterNewSurfaces();
    }
  }
  GarbageCollectSurfaces();
}

The range-for loop captures begin() and end() iterators of it->second, a std::vector<raw_ptr<SurfaceAllocationGroup, VectorExperimental>>. These iterators point into the vector’s internal heap buffer.

WillNotRegisterNewSurfaces moves out all blocked embedders and synchronously resolves their activation dependencies:

// components/viz/service/surfaces/surface_allocation_group.cc:176-182
void SurfaceAllocationGroup::WillNotRegisterNewSurfaces() {
  base::flat_map<Surface*, SurfaceId> embedders = std::move(blocked_embedders_);
  blocked_embedders_.clear();
  for (const auto& entry : embedders) {
    entry.first->OnActivationDependencyResolved(entry.second, this);
  }
}

If the resolved dependency was the last outstanding one for a surface, OnActivationDependencyResolved calls ActivatePendingFrame, which calls ActivateFrame, which calls RecomputeActiveReferencedSurfaces. That function iterates the activated frame’s referenced_surfaces metadata and calls GetOrCreateAllocationGroupForSurfaceId for each entry:

// components/viz/service/surfaces/surface_manager.cc:656-661
if (!allocation_group) {
    allocation_group = std::make_unique<SurfaceAllocationGroup>(
        this, surface_id.frame_sink_id(),
        surface_id.local_surface_id().embed_token());
    frame_sink_id_to_allocation_groups_[surface_id.frame_sink_id()].push_back(
        allocation_group.get());
}

When surface_id.frame_sink_id() matches the FrameSinkId currently being invalidated, push_back appends to the same vector that InvalidateFrameSinkId is iterating. If the vector’s capacity is exceeded, it reallocates its internal buffer, freeing the old one. The range-for loop then advances its stale iterator into the freed buffer, reading a dangling SurfaceAllocationGroup* pointer.

A compromised renderer can construct this state through normal Mojo interfaces. It creates a target FrameSink X and three attacker FrameSinks via blink.mojom.EmbeddedFrameSinkProvider.CreateSimpleCompositorFrameSink. Each attacker submits a CompositorFrame with one activation_dependency referencing X (using distinct embed tokens E1, E2, E3), which creates three allocation groups in the vector for X. The first attacker’s frame additionally carries 200 referenced_surfaces entries, each referencing X with a unique embed token. These entries are inert while the frame is pending. When the renderer closes X’s EmbeddedFrameSinkClient connection, the browser forwards an InvalidateFrameSinkId(X) call to the Viz process. The loop processes E1’s group first, resolving the first attacker surface’s sole dependency, activating its pending frame, and processing 200 referenced_surfaces entries. Each entry calls GetOrCreateAllocationGroupForSurfaceId, which push_backs a new group into the vector for X. The vector grows from 3 elements to over 200, reallocating its buffer. The next loop iteration dereferences the stale iterator into the freed 3-element buffer.

No mitigations block this path. There are no CHECK guards on the iteration, no reentrancy protection, and no copy of the vector before iteration. The raw_ptr<VectorExperimental> wrapper on the vector elements protects the pointed-to SurfaceAllocationGroup objects but does not protect the vector’s own internal buffer from iterator invalidation. The mojom deserialization in compositor_frame_metadata_mojom_traits.cc imposes no restrictions on the number or content of referenced_surfaces or activation_dependencies. ASAN confirms “MiraclePtr Status: NOT PROTECTED” for this access.

Reproduce

Tested at commit 7c89d33808e551aed6122c1f324864784011c158.

Apply the attached patch.diff to the renderer source, then build:

git apply issue_framesink_invalidation_uaf/patch.diff
autoninja -C ~/chromium/src/out/asan-release chrome

Launch and open poc.html:

ASAN_OPTIONS=detect_odr_violation=0 xvfb-run -a \
  ~/chromium/src/out/asan-release/chrome \
  --user-data-dir=/tmp/poc-$(date +%s) \
  issue_framesink_invalidation_uaf/poc.html

The GPU/Viz process crashes with a heap-use-after-free within approximately one second. The crash occurs on the VizCompositorThread inside SurfaceManager::InvalidateFrameSinkId.

==2458553==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b2e9da4f4c8 at pc 0x7efea76d1825 bp 0x7afe82a9eb30 sp 0x7afe82a9eb28
READ of size 8 at 0x7b2e9da4f4c8 thread T15 (VizCompositorTh)
    #0 0x7efea76d1824 in viz::SurfaceManager::InvalidateFrameSinkId(viz::FrameSinkId const&) base/allocator/partition_allocator/src/partition_alloc/pointers/raw_ptr.h:1018:47
    #1 0x7efea75774b5 in viz::FrameSinkManagerImpl::InvalidateFrameSinkId(viz::FrameSinkId const&, base::OnceCallback<void ()>) components/viz/service/frame_sinks/frame_sink_manager_impl.cc:223:20
    #2 0x7efea77696ae in viz::mojom::FrameSinkManagerStubDispatch::AcceptWithResponder(viz::mojom::FrameSinkManager*, mojo::Message*, std::__Cr::unique_ptr<mojo::MessageReceiverWithStatus, std::__Cr::default_delete<mojo::MessageReceiverWithStatus>>) gen/services/viz/privileged/mojom/compositing/frame_sink_manager.mojom.cc:3271:13

0x7b2e9da4f4c8 is located 8 bytes inside of 32-byte region [0x7b2e9da4f4c0,0x7b2e9da4f4e0)
freed by thread T15 (VizCompositorTh) here:
    #0 0x5596ce120b02 in operator delete(void*, unsigned long)
    #1 0x7efea76db70d in std::__Cr::vector<...>::__emplace_back_slow_path gen/third_party/libc++/src/include/__new/allocate.h:63:10
    #2 0x7efea76cfed8 in viz::SurfaceManager::GetOrCreateAllocationGroupForSurfaceId(viz::SurfaceId const&) gen/third_party/libc++/src/include/__vector/vector.h:1148:21
    #3 0x7efea76a888f in viz::Surface::RecomputeActiveReferencedSurfaces() components/viz/service/surfaces/surface.cc:634:27
    #4 0x7efea76ad04e in viz::Surface::ActivateFrame(viz::Surface::FrameData) components/viz/service/surfaces/surface.cc:697:3
    #5 0x7efea76a7df9 in viz::Surface::ActivatePendingFrame() components/viz/service/surfaces/surface.cc:520:3
    #6 0x7efea76af659 in viz::Surface::OnActivationDependencyResolved(viz::SurfaceId const&, viz::SurfaceAllocationGroup*) components/viz/service/surfaces/surface.cc:459:3
    #7 0x7efea76cb544 in viz::SurfaceAllocationGroup::WillNotRegisterNewSurfaces() components/viz/service/surfaces/surface_allocation_group.cc:180:18
    #8 0x7efea76d1781 in viz::SurfaceManager::InvalidateFrameSinkId(viz::FrameSinkId const&) components/viz/service/surfaces/surface_manager.cc:170:14

previously allocated by thread T15 (VizCompositorTh) here:
    #0 0x5596ce11fefd in operator new(unsigned long)
    #1 0x7efea76db5e4 in std::__Cr::vector<...>::__emplace_back_slow_path gen/third_party/libc++/src/include/__new/allocate.h:43:28
    #2 0x7efea76cfed8 in viz::SurfaceManager::GetOrCreateAllocationGroupForSurfaceId(viz::SurfaceId const&) gen/third_party/libc++/src/include/__vector/vector.h:1148:21
    #3 0x7efea76ac1d6 in viz::Surface::UpdateActivationDependencies(viz::CompositorFrame const&) components/viz/service/surfaces/surface.cc:802:27
    #4 0x7efea76aab53 in viz::Surface::CommitFrame(viz::Surface::FrameData) components/viz/service/surfaces/surface.cc:343:3
    #5 0x7efea76a9e4a in viz::Surface::QueueFrame(viz::CompositorFrame, unsigned int, base::ScopedClosureRunner) components/viz/service/surfaces/surface.cc:270:14
    #6 0x7efea7541176 in viz::CompositorFrameSinkSupport::MaybeSubmitCompositorFrame components/viz/service/frame_sinks/compositor_frame_sink_support.cc:1014:55

MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.

The complete ASAN log is in asan.log.

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker