Critical patch-now ✔︎ verified CVSS 8.3 chrome UAF 🔧 Commit mapped 🧬 A nested callout frees the object, then it is used

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Compositing
DescriptionUse after free in Compositing
ComponentCompositing
Sub-componentViz
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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
Reviewer notes: none; diff (local copy of referenced_surfaces, added re-entrancy comment, and regression test) matches the claimed modify-during-iteration UAF in RecomputeActiveReferencedSurfaces, and escalation beyond the UAF is appropriately marked inferred.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker514442821
Fix commit4c246406e3da (chromium/src) +71/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-18 (per vendor advisory)
Fix landed2026-07-14 (public git)
Disclosed2026-07-29
Patch gap15 days — fix public in source before the advisory

Background

Surface
A Viz-side object holding a client’s CompositorFrame data; a Surface can hold both an active_frame_data_ (the frame currently used for drawing) and a pending frame awaiting its dependencies.
Referenced surfaces
The set of SurfaceRanges in a frame’s metadata.referenced_surfaces that identify other surfaces this frame embeds, used by SurfaceManager to track cross-surface references.
Activation dependency
A SurfaceId a pending frame is blocked on; when a referenced child surface activates it can synchronously resolve a parent’s dependency and promote the parent’s pending frame to active.
Sync re-entrancy
Re-entering surface bookkeeping code from within an ongoing surface operation, here because activating one surface synchronously drives activation of another during the same call stack.

Root Cause Analysis

The vulnerable code in Surface::RecomputeActiveReferencedSurfaces (named in the commit message; the @@ -614 hunk header does not carry the function name) iterated directly over active_frame_data_->frame.metadata.referenced_surfaces by reference. Inside the loop it called UpdateLastActiveReferenceAndMaybeActivate() (per the added comment), which can synchronously activate a referenced child surface; that activation can in turn resolve an activation dependency of this surface’s own pending frame, causing the pending frame to be promoted and active_frame_data_ to be overwritten mid-iteration. Because the loop held a reference into the vector owned by the now-replaced active_frame_data_, the container it was walking was freed out from under it, leaving the iterator and the SurfaceRange reference dangling — a classic modify-during-iteration use-after-free.

The fix takes a local copy, const std::vector<SurfaceRange> referenced_surfaces = active_frame_data_->frame.metadata.referenced_surfaces;, and iterates over that copy, so the loop’s backing storage survives regardless of what happens to active_frame_data_ during re-entrant activation.

The added regression test PendingFrameActivatesWhileRecomputingReferences constructs exactly this chain and asserts the parent’s active frame is safely replaced without crashing.

Key insight
The single mistake is iterating over a container reachable through active_frame_data_ while calling into code that can synchronously replace active_frame_data_ (and thus free that container); the fix eliminates the dangling access by snapshotting referenced_surfaces into a stack-local copy before the loop.

Attack Path

  1. Set up an active parent embedding multiple child groups A client submits a parent CompositorFrame referencing several child allocation groups (e.g. child_id1a, child_id2) that do not yet exist, so it activates immediately and the parent becomes an active embedder of those groups.
  2. Queue a pending parent frame blocked on a child The client submits a second parent frame whose activation dependency is a specific child (child_id1a); it stays pending while the first frame remains the parent’s active frame, so the parent now holds both an active and a pending frame.
  3. Arm a pending child that can be force-activated A frame is submitted to that child (child_id1a) depending on a surface that will never exist, leaving the child pending but eligible to be force-activated when the parent later processes its reference.
  4. Trigger re-entrant activation during recomputation Activating an unrelated child (child_id2) notifies the parent and runs Surface::RecomputeActiveReferencedSurfaces; while iterating references it force-activates child_id1a, which resolves the parent’s pending dependency and replaces active_frame_data_ while the loop still references the old frame’s referenced_surfaces vector, producing the use-after-free.
  5. Escalate via the freed allocation Controlling the timing and contents of the reallocated memory to turn the dangling read/iteration into a controllable corruption primitive is a further step the diff does not demonstrate (inferred).

Impact Assessment

An attacker who can submit crafted CompositorFrames (a compromised or malicious renderer driving Viz surface synchronization) can trigger a use-after-free in the Viz compositing service, which runs in the GPU process. The preconditions are the specific frame-submission sequence that creates a parent with both active and pending frames plus a referenced child that force-activates during reference recomputation, as encoded in the regression test. The advisory classifies this as a use-after-free in Compositing; any escalation to memory disclosure, controllable corruption, or code execution in the GPU process is (inferred) and not established by the patch, and the advisory provided states no exploited-in-the-wild status.

Changed Functions

FunctionChangeNotes
for
components/viz/service/surfaces/surface.cc
modified

Files Changed

  • components/viz/service/frame_sinks/surface_synchronization_unittest.cc
  • components/viz/service/surfaces/surface.cc

Audit Directions

  • Iterate-then-callback over owned containers
    Audit loops that walk a vector or map reachable through a member pointer (like active_frame_data_) while calling into code that can synchronously mutate or replace that member; prefer snapshotting or index-based revalidation.
  • Synchronous surface activation re-entrancy
    Review other Viz Surface/SurfaceManager paths where activation of one surface can cascade into activation or destruction of another within the same call stack (e.g. dependency resolution, UpdateLastActiveReferenceAndMaybeActivate callers) for stale references to pre-callback state.
  • Active-vs-pending frame swaps
    Check code assuming active_frame_data_ is stable across a callback boundary, since pending-to-active promotion can invalidate any reference, iterator, or cached pointer derived from the previous active frame.
From 4c246406e3da54bcb173894181a664b84450dcd9 Mon Sep 17 00:00:00 2001
From: Saifuddin Hitawala <[email protected]>
Date: Tue, 14 Jul 2026 08:29:03 -0700
Subject: [PATCH] [viz] Fix UAF through surface re-entrancy for referenced surfaces

Guard against UAF through vector modification over sync re-entrancy
in Surface::RecomputeActiveReferencedSurfaces. This can happen when
a dependent surface gets activated which causes referenced surfaces
to be recomputed and a pending surface on same frame to be activated
which overwrites the currently active frame's data and leads to
dangling refs. This change makes a local copy and adds unittest to
verify this.

Bug: 514442821
Change-Id: I5093537dbb0f627265c715ca326629db4f417d7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8085883
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: Saifuddin Hitawala <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661877}
---

diff --git a/components/viz/service/frame_sinks/surface_synchronization_unittest.cc b/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
index 8ae9bdf0..d98140b 100644
--- a/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
+++ b/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
@@ -3575,6 +3575,70 @@
   EXPECT_TRUE(child_surface1()->HasUnackedActiveFrame());
 }
 
+// Tests that the parent's active frame can be replaced by a pending frame while
+// the parent is recomputing its referenced surfaces. This happens when
+// processing a referenced surface causes a child to activate, which in turn
+// resolves the activation dependency of the parent's pending frame.
+TEST_F(SurfaceSynchronizationTest,
+       PendingFrameActivatesWhileRecomputingReferences) {
+  const SurfaceId parent_id = MakeSurfaceId(kParentFrameSink, 1);
+  // Two SurfaceIds in the same allocation group with incomparable sequence
+  // numbers so that neither is considered newer than the other.
+  const SurfaceId child_id1a = MakeSurfaceId(kChildFrameSink1, 1, 2);
+  const SurfaceId child_id1b = MakeSurfaceId(kChildFrameSink1, 2, 1);
+  const SurfaceId child_id2 = MakeSurfaceId(kChildFrameSink2, 1);
+  const SurfaceId arbitrary_id = MakeSurfaceId(kArbitraryFrameSink, 1);
+
+  // Submit the first parent frame referencing both child allocation groups.
+  // Neither child exists yet so the frame activates immediately and the parent
+  // becomes an active embedder of both groups.
+  parent_support().SubmitCompositorFrame(
+      parent_id.local_surface_id(),
+      MakeCompositorFrame(empty_surface_ids(),
+                          {SurfaceRange(std::nullopt, child_id1a),
+                           SurfaceRange(std::nullopt, child_id2),
+                           SurfaceRange(std::nullopt, child_id1b),
+                           SurfaceRange(std::nullopt, arbitrary_id)},
+                          std::vector<TransferableResource>()));
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_FALSE(parent_surface()->HasPendingFrame());
+  // Submit a second parent frame that depends on |child_id1a|. It stays
+  // pending and the parent becomes a blocked embedder of |child_id1a|'s
+  // allocation group.
+  parent_support().SubmitCompositorFrame(
+      parent_id.local_surface_id(),
+      MakeCompositorFrame({child_id1a}, empty_surface_ranges(),
+                          std::vector<TransferableResource>(),
+                          MakeDefaultDeadline()));
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_TRUE(parent_surface()->HasPendingFrame());
+  EXPECT_THAT(parent_surface()->activation_dependencies(),
+              UnorderedElementsAre(child_id1a));
+  // Submit a frame to |child_id1a| that depends on a surface that will never
+  // exist. It stays pending.
+  child_support1().SubmitCompositorFrame(
+      child_id1a.local_surface_id(),
+      MakeCompositorFrame({arbitrary_id}, empty_surface_ranges(),
+                          std::vector<TransferableResource>(),
+                          MakeDefaultDeadline()));
+  EXPECT_FALSE(child_surface1()->HasActiveFrame());
+  EXPECT_TRUE(child_surface1()->HasPendingFrame());
+  // Submit a frame to |child_id2| with no dependencies. Its activation
+  // notifies the parent, which recomputes its referenced surfaces. While
+  // processing the |child_id1a| reference, that child is force-activated which
+  // in turn resolves the dependency of the parent's pending frame, replacing
+  // the parent's active frame mid-iteration. This must not crash.
+  child_support2().SubmitCompositorFrame(
+      child_id2.local_surface_id(),
+      MakeCompositorFrame(empty_surface_ids(), empty_surface_ranges(),
+                          std::vector<TransferableResource>()));
+
+  EXPECT_TRUE(child_surface1()->HasActiveFrame());
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_FALSE(parent_surface()->HasPendingFrame());
+  EXPECT_THAT(parent_surface()->activation_dependencies(), IsEmpty());
+}
+
 // Tests that when a CompositorFrame for an Embedded Surface arrives after its
 // Embedder has submitted new ActivationDependencies, that it is immediately
 // ACKed, even if normally it would not be due to damage. This way we don't have
diff --git a/components/viz/service/surfaces/surface.cc b/components/viz/service/surfaces/surface.cc
index 980f51a..0abab86 100644
--- a/components/viz/service/surfaces/surface.cc
+++ b/components/viz/service/surfaces/surface.cc
@@ -614,8 +614,13 @@
   // notify SurfaceManager of the new references.
   active_referenced_surfaces_.clear();
   std::vector<SurfaceAllocationGroup*> new_referenced_allocation_groups;
-  for (const SurfaceRange& surface_range :
-       active_frame_data_->frame.metadata.referenced_surfaces) {
+  // Iterate over a copy because UpdateLastActiveReferenceAndMaybeActivate()
+  // can synchronously activate a referenced surface, which can resolve a
+  // dependency of this surface's pending frame and replace
+  // |active_frame_data_| while we're iterating over it.
+  const std::vector<SurfaceRange> referenced_surfaces =
+      active_frame_data_->frame.metadata.referenced_surfaces;
+  for (const SurfaceRange& surface_range : referenced_surfaces) {
     // Figure out what surface in the |surface_range| needs to be referenced.
     Surface* surface =
         surface_manager_->GetLatestInFlightSurface(surface_range);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/viz/service/frame_sinks/surface_synchronization_unittest.cc b/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
index 8ae9bdf0..d98140b 100644
--- a/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
+++ b/components/viz/service/frame_sinks/surface_synchronization_unittest.cc
@@ -3575,6 +3575,70 @@
   EXPECT_TRUE(child_surface1()->HasUnackedActiveFrame());
 }
 
+// Tests that the parent's active frame can be replaced by a pending frame while
+// the parent is recomputing its referenced surfaces. This happens when
+// processing a referenced surface causes a child to activate, which in turn
+// resolves the activation dependency of the parent's pending frame.
+TEST_F(SurfaceSynchronizationTest,
+       PendingFrameActivatesWhileRecomputingReferences) {
+  const SurfaceId parent_id = MakeSurfaceId(kParentFrameSink, 1);
+  // Two SurfaceIds in the same allocation group with incomparable sequence
+  // numbers so that neither is considered newer than the other.
+  const SurfaceId child_id1a = MakeSurfaceId(kChildFrameSink1, 1, 2);
+  const SurfaceId child_id1b = MakeSurfaceId(kChildFrameSink1, 2, 1);
+  const SurfaceId child_id2 = MakeSurfaceId(kChildFrameSink2, 1);
+  const SurfaceId arbitrary_id = MakeSurfaceId(kArbitraryFrameSink, 1);
+
+  // Submit the first parent frame referencing both child allocation groups.
+  // Neither child exists yet so the frame activates immediately and the parent
+  // becomes an active embedder of both groups.
+  parent_support().SubmitCompositorFrame(
+      parent_id.local_surface_id(),
+      MakeCompositorFrame(empty_surface_ids(),
+                          {SurfaceRange(std::nullopt, child_id1a),
+                           SurfaceRange(std::nullopt, child_id2),
+                           SurfaceRange(std::nullopt, child_id1b),
+                           SurfaceRange(std::nullopt, arbitrary_id)},
+                          std::vector<TransferableResource>()));
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_FALSE(parent_surface()->HasPendingFrame());
+  // Submit a second parent frame that depends on |child_id1a|. It stays
+  // pending and the parent becomes a blocked embedder of |child_id1a|'s
+  // allocation group.
+  parent_support().SubmitCompositorFrame(
+      parent_id.local_surface_id(),
+      MakeCompositorFrame({child_id1a}, empty_surface_ranges(),
+                          std::vector<TransferableResource>(),
+                          MakeDefaultDeadline()));
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_TRUE(parent_surface()->HasPendingFrame());
+  EXPECT_THAT(parent_surface()->activation_dependencies(),
+              UnorderedElementsAre(child_id1a));
+  // Submit a frame to |child_id1a| that depends on a surface that will never
+  // exist. It stays pending.
+  child_support1().SubmitCompositorFrame(
+      child_id1a.local_surface_id(),
+      MakeCompositorFrame({arbitrary_id}, empty_surface_ranges(),
+                          std::vector<TransferableResource>(),
+                          MakeDefaultDeadline()));
+  EXPECT_FALSE(child_surface1()->HasActiveFrame());
+  EXPECT_TRUE(child_surface1()->HasPendingFrame());
+  // Submit a frame to |child_id2| with no dependencies. Its activation
+  // notifies the parent, which recomputes its referenced surfaces. While
+  // processing the |child_id1a| reference, that child is force-activated which
+  // in turn resolves the dependency of the parent's pending frame, replacing
+  // the parent's active frame mid-iteration. This must not crash.
+  child_support2().SubmitCompositorFrame(
+      child_id2.local_surface_id(),
+      MakeCompositorFrame(empty_surface_ids(), empty_surface_ranges(),
+                          std::vector<TransferableResource>()));
+
+  EXPECT_TRUE(child_surface1()->HasActiveFrame());
+  EXPECT_TRUE(parent_surface()->HasActiveFrame());
+  EXPECT_FALSE(parent_surface()->HasPendingFrame());
+  EXPECT_THAT(parent_surface()->activation_dependencies(), IsEmpty());
+}
+
 // Tests that when a CompositorFrame for an Embedded Surface arrives after its
 // Embedder has submitted new ActivationDependencies, that it is immediately
 // ACKed, even if normally it would not be due to damage. This way we don't have
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.