High CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Sub-componentUi/compositor
Bug ClassUAF
Tracker501524262
Fix commit6eb376c4e511 (chromium/src) +85/-5
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-04-10 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-05-27
Patch gap12 days — fix public in source before the advisory

Background

`ui::LayerAnimationSequence`
a browser-process object (ui/compositor/) representing an ordered list of LayerAnimationElements applied to a compositor Layer. Its Progress()/ProgressToEnd()/Abort()/Start() methods drive the elements and notify a LayerAnimationDelegate (typically a ui::Layer).
Re-entrant deletion
when an element calls into the delegate (e.g. SetBoundsFromAnimation, SetBrightnessFromAnimation), the Layer synchronously notifies observers (views::View, aura::Window). An observer may call LayerAnimator::StopAnimating(), whose FinishAnimation moves the running sequence into a local std::unique_ptr that is destroyed on return — freeing the sequence mid-method.
Implicit `this`
after such an out-call, the sequence’s own methods keep touching member variables (elements_, last_element_, last_progressed_fraction_) through this, which now points at freed memory.
`base::WeakPtr` / `AsWeakPtr()`
the standard Chromium liveness guard: take a weak pointer before an out-call, and bail if it is invalidated by re-entrant destruction. BackupRefPtr does not protect this path because the access is via the raw implicit this, not a raw_ptr.

Root Cause Analysis

The invariant is that after any call that can re-enter the animator and destroy the sequence, LayerAnimationSequence must not touch its own members. Progress() already took base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr()) around the per-frame elements_[current_index]->Progress(now, delegate) out-call — but it had no guard around the earlier elements_[current_index]->ProgressToEnd(delegate) out-call in the “let the element we’re passing finish” branch of the while loop. ProgressToEnd notifies the delegate → Layer → observers; an observer that stops/preempts the animation triggers LayerAnimator::FinishAnimation, which destroys the sequence. Control then returns into the loop, which executes last_start_ += element_duration; ++last_element_; last_progressed_fraction_ = ... on freed memory, and on the next iteration may make a virtual call on a freed element — a use-after-free in the browser process. The same unguarded-out-call pattern existed in the element-finish loops of ProgressToEnd() itself and in Abort(), and in the Start() call near the top of the sequence.

The patch installs base::WeakPtr liveness checks around every re-entrant out-call. In LayerAnimationSequence::Progress (ui/compositor/layer_animation_sequence.cc) the alive weak pointer is created once before the loop; a if (!alive) return; is added immediately after the ProgressToEnd(delegate) call, and the existing per-frame Progress call is restructured so the liveness is checked both before and after it. Start() gains alive(AsWeakPtr()) around elements_[0]->Start(...) with an early return. ProgressToEnd() and Abort() each take alive before their element loops and return if the weak pointer is cleared after an element out-call. The regression test LayerAnimatorTest.ProgressToEndInWhileLoopFreesSequenceUAF with a PreemptingDelegate re-enters via StopAnimatingProperty(BRIGHTNESS) during SetBrightnessFromAnimation, reproducing the exact free-and-continue path. The advisory’s “Use-after-free” (CWE-416) is confirmed by the diff.

Key insight
LayerAnimationSequence::Progress guarded only its per-frame Progress out-call with a WeakPtr check and left the earlier ProgressToEnd out-call (and the analogous calls in ProgressToEnd, Abort, and Start) unguarded — so a re-entrant observer that stopped the animation could free the sequence while the method kept using this.

Attack Path

  1. Trigger a browser-process UI animation from a compromised renderer, drive DOM/UI actions that start a Layer animation (window resize, tabstrip/brightness-style property animations).
  2. Arm a preempting observer arrange UI state so that a View/Window observer, on receiving the property-change notification during an element’s ProgressToEnd, synchronously calls LayerAnimator::StopAnimating()/StopAnimatingProperty().
  3. Free the sequence the stop path runs FinishAnimation, moving the sequence into a local unique_ptr that is destroyed when it returns, freeing the LayerAnimationSequence and its elements mid-loop.
  4. Reclaim (inferred) concurrently spray the browser-process heap (e.g. via Mojo BlobRegistry/data pipes) to place attacker-controlled bytes into the freed chunk.
  5. Control-flow hijack (inferred) when Progress() resumes it reads the reclaimed elements_ and invokes a virtual method on a forged element, dereferencing an attacker vtable — a browser-process RCE / sandbox escape.

Impact Assessment

The UAF is in the browser process, the highest-value target (a successful exploit is a sandbox escape). It is reachable by driving browser-side UI animations, which a compromised or malicious renderer can influence, but the trigger depends on an observer re-entrantly stopping the animation at the right moment. The diff substantiates the use-after-free (confirmed by the ASAN regression test); the heap-spray reclamation and virtual-call hijack to full code execution are (inferred) from the report and not shown by this diff. BackupRefPtr does not mitigate it because the dangling access is through the implicit this, not a raw_ptr member. CVSS 8.3 (S:C) reflects the browser-process/sandbox-escape potential.

Changed Functions

FunctionChangeNotes
if
ui/compositor/layer_animation_sequence.cc
modified
PreemptingDelegate
ui/compositor/layer_animator_unittest.cc
modified
TEST
ui/compositor/layer_animator_unittest.cc
modified

Files Changed

  • ui/compositor/layer_animation_sequence.cc
  • ui/compositor/layer_animator_unittest.cc

Audit Directions

  • Unguarded delegate/element out-calls
    audit all of ui/compositor and ui/views animation code for methods that call into a delegate/observer and then touch this/members without a WeakPtr recheck; this class of re-entrancy UAF is easy to reintroduce.
  • `FinishAnimation`/`StopAnimating` re-entrancy
    map every call site that can re-enter LayerAnimator and destroy the running sequence, and confirm each caller is guarded.
  • Observer notification during property set
    Layer::Set*FromAnimation synchronously notifies observers; review other animated-property setters for callers that assume the sequence survives the notification.
  • Implicit-`this` UAFs vs MiraclePtr
    because BRP cannot protect implicit this, flag long-lived objects that make synchronous observer out-calls mid-method as candidates for WeakPtr-guarded patterns.
From 6eb376c4e511ff1e8ca1b802cec5cbbb5df7ab85 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Thu, 14 May 2026 21:11:30 -0700
Subject: [PATCH] [ui/compositor] Fix heap-use-after-free in LayerAnimationSequence

This CL fixes a heap-use-after-free bug in LayerAnimationSequence where
the sequence could be destroyed re-entrantly during its own execution.

The vulnerability existed in several methods (Progress, ProgressToEnd,
Abort, and Start) that call out to the LayerAnimationDelegate or its
LayerAnimationElements. These out-calls can trigger observers (e.g.,
aura::WindowObserver or LayoutManager) to re-enter the LayerAnimator
and stop or preempt the running sequence, causing it to be deleted.

When control returned to LayerAnimationSequence, it would continue
accessing member variables or iterating through its elements, resulting
in a use-after-free.

Changes:
- Added base::WeakPtr<LayerAnimationSequence> guards around all
  potentially re-entrant calls in LayerAnimationSequence::Progress,
  ProgressToEnd, Abort, and Start.
- The sequence now early-returns or breaks out of loops if it detects
  it has been destroyed during an out-call.
- Added a regression test ProgressToEndInWhileLoopFreesSequenceUAF in
  layer_animator_unittest.cc which specifically reproduces the UAF
  via a re-entrant StopAnimatingProperty call.

Fixed: 501524262
Change-Id: I6f7d68edbd0230770c4a2dc4472a4fde133a139b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7846156
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1631050}
---

diff --git a/ui/compositor/layer_animation_sequence.cc b/ui/compositor/layer_animation_sequence.cc
index 6d793ff51..ffa98f8 100644
--- a/ui/compositor/layer_animation_sequence.cc
+++ b/ui/compositor/layer_animation_sequence.cc
@@ -57,7 +57,11 @@
          "indefinite amount of time without any actual animated content";
 
   elements_[0]->set_requested_start_time(start_time_);
+  base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
   elements_[0]->Start(delegate, animation_group_id_);
+  if (!alive) {
+    return;
+  }
 
   NotifyStarted();
 
@@ -75,6 +79,8 @@
   if (last_element_ == 0)
     last_start_ = start_time_;
 
+  base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
+
   const base::TimeDelta total_duration = GetTotalDurationOfAllElements();
   const auto animation_should_progress = [this, total_duration]() {
     // A repeating animation with zero total duration results in an infinite
@@ -95,6 +101,11 @@
     // Let the element we're passing finish.
     if (elements_[current_index]->ProgressToEnd(delegate))
       redraw_required = true;
+
+    if (!alive) {
+      return;
+    }
+
     last_start_ += element_duration;
     ++last_element_;
     last_progressed_fraction_ =
@@ -109,15 +120,22 @@
       animation_group_id_ = cc::AnimationIdProvider::NextGroupId();
       elements_[current_index]->Start(delegate, animation_group_id_);
     }
-    base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
-    if (elements_[current_index]->Progress(now, delegate))
-      redraw_required = true;
-    if (!alive)
+
+    if (!alive) {
       return;
+    }
+
+    if (elements_[current_index]->Progress(now, delegate)) {
+      redraw_required = true;
+    }
+
+    if (!alive) {
+      return;
+    }
+
     last_progressed_fraction_ =
         elements_[current_index]->last_progressed_fraction();
   }
-
   // Since the delegate may be deleted due to the notifications below, it is
   // important that we schedule a draw before sending them.
   if (redraw_required)
@@ -166,10 +184,17 @@
   if (elements_.empty())
     return;
 
+  base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
+
   size_t current_index = last_element_ % elements_.size();
   while (current_index < elements_.size()) {
     if (elements_[current_index]->ProgressToEnd(delegate))
       redraw_required = true;
+
+    if (!alive) {
+      return;
+    }
+
     last_progressed_fraction_ =
         elements_[current_index]->last_progressed_fraction();
     ++current_index;
@@ -199,9 +224,13 @@
 }
 
 void LayerAnimationSequence::Abort(LayerAnimationDelegate* delegate) {
+  base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
   size_t current_index = last_element_ % elements_.size();
   while (current_index < elements_.size()) {
     elements_[current_index]->Abort(delegate);
+    if (!alive) {
+      return;
+    }
     ++current_index;
   }
   last_element_ = 0;
diff --git a/ui/compositor/layer_animator_unittest.cc b/ui/compositor/layer_animator_unittest.cc
index 53c9aa7..1b9d4fb648 100644
--- a/ui/compositor/layer_animator_unittest.cc
+++ b/ui/compositor/layer_animator_unittest.cc
@@ -3825,4 +3825,55 @@
   }
 }
 
+// Regression test for heap-use-after-free in LayerAnimationSequence::Progress.
+class PreemptingDelegate : public TestLayerAnimationDelegate {
+ public:
+  PreemptingDelegate() = default;
+  ~PreemptingDelegate() override = default;
+
+  void set_animator(LayerAnimator* animator) { animator_ = animator; }
+  void Arm() { armed_ = true; }
+
+  void SetBrightnessFromAnimation(float brightness,
+                                  PropertyChangeReason reason) override {
+    TestLayerAnimationDelegate::SetBrightnessFromAnimation(brightness, reason);
+    // Re-enter the animator to stop the sequence and trigger deletion.
+    if (!armed_ || reentered_ ||
+        reason != PropertyChangeReason::FROM_ANIMATION) {
+      return;
+    }
+    reentered_ = true;
+    animator_->StopAnimatingProperty(LayerAnimationElement::BRIGHTNESS);
+  }
+
+ private:
+  raw_ptr<LayerAnimator> animator_ = nullptr;
+  bool armed_ = false;
+  bool reentered_ = false;
+};
+
+TEST(LayerAnimatorTest, ProgressToEndInWhileLoopFreesSequenceUAF) {
+  scoped_refptr<LayerAnimator> animator = CreateDefaultTestAnimator(nullptr);
+  PreemptingDelegate delegate;
+  animator->SetDelegate(&delegate);
+  delegate.set_animator(animator.get());
+
+  // Create a two-element sequence where the first element is finished on
+  // Step().
+  LayerAnimationSequence* seq = new LayerAnimationSequence();
+  seq->AddElement(LayerAnimationElement::CreateBrightnessElement(
+      0.5f, base::Milliseconds(10)));
+  seq->AddElement(LayerAnimationElement::CreateBrightnessElement(
+      1.0f, base::Milliseconds(1000)));
+
+  animator->StartAnimation(seq);
+  ASSERT_TRUE(animator->is_animating());
+
+  base::TimeTicks start_time = animator->last_step_time();
+  delegate.Arm();
+
+  // Step far enough to finish the first element and trigger the UAF path.
+  animator->Step(start_time + base::Milliseconds(50));
+}
+
 }  // namespace ui
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/compositor/layer_animator_unittest.cc b/ui/compositor/layer_animator_unittest.cc
index 53c9aa7..1b9d4fb648 100644
--- a/ui/compositor/layer_animator_unittest.cc
+++ b/ui/compositor/layer_animator_unittest.cc
@@ -3825,4 +3825,55 @@
   }
 }
 
+// Regression test for heap-use-after-free in LayerAnimationSequence::Progress.
+class PreemptingDelegate : public TestLayerAnimationDelegate {
+ public:
+  PreemptingDelegate() = default;
+  ~PreemptingDelegate() override = default;
+
+  void set_animator(LayerAnimator* animator) { animator_ = animator; }
+  void Arm() { armed_ = true; }
+
+  void SetBrightnessFromAnimation(float brightness,
+                                  PropertyChangeReason reason) override {
+    TestLayerAnimationDelegate::SetBrightnessFromAnimation(brightness, reason);
+    // Re-enter the animator to stop the sequence and trigger deletion.
+    if (!armed_ || reentered_ ||
+        reason != PropertyChangeReason::FROM_ANIMATION) {
+      return;
+    }
+    reentered_ = true;
+    animator_->StopAnimatingProperty(LayerAnimationElement::BRIGHTNESS);
+  }
+
+ private:
+  raw_ptr<LayerAnimator> animator_ = nullptr;
+  bool armed_ = false;
+  bool reentered_ = false;
+};
+
+TEST(LayerAnimatorTest, ProgressToEndInWhileLoopFreesSequenceUAF) {
+  scoped_refptr<LayerAnimator> animator = CreateDefaultTestAnimator(nullptr);
+  PreemptingDelegate delegate;
+  animator->SetDelegate(&delegate);
+  delegate.set_animator(animator.get());
+
+  // Create a two-element sequence where the first element is finished on
+  // Step().
+  LayerAnimationSequence* seq = new LayerAnimationSequence();
+  seq->AddElement(LayerAnimationElement::CreateBrightnessElement(
+      0.5f, base::Milliseconds(10)));
+  seq->AddElement(LayerAnimationElement::CreateBrightnessElement(
+      1.0f, base::Milliseconds(1000)));
+
+  animator->StartAnimation(seq);
+  ASSERT_TRUE(animator->is_animating());
+
+  base::TimeTicks start_time = animator->last_step_time();
+  delegate.Arm();
+
+  // Step far enough to finish the first element and trigger the UAF path.
+  animator->Step(start_time + base::Milliseconds(50));
+}
+
 }  // namespace ui
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in LayerAnimationSequence::Progress via re-entrant deletion

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A potential Use-After-Free exists in LayerAnimationSequence::Progress() due to an unguarded outcall to ProgressToEnd(). Synchronous observer callbacks during this outcall can re-entrantly destroy the animation sequence, leading to member accesses on freed memory and potential browser process code execution.

Affected files:

  • ui/compositor/layer_animation_sequence.cc

Estimated timestamp from git blame: 2021-02-25

Vulnerability Details

A potential Use-After-Free (UAF) vulnerability exists in ui/compositor/layer_animation_sequence.cc. The LayerAnimationSequence::Progress() method processes a sequence of animation elements in a while loop. At line 96, it makes an unguarded outcall:

    // Let the element we're passing finish.
    if (elements_[current_index]->ProgressToEnd(delegate))
      redraw_required = true;
    last_start_ += element_duration;
    ++last_element_;
    // ...

When ProgressToEnd(delegate) is called on the element, it notifies the delegate (typically a ui::Layer) to update its properties (e.g., SetBoundsFromAnimation). The Layer then synchronously notifies its observers (e.g., views::View or aura::Window components) of the property change.

If an observer reacts to this notification by aborting the animation or starting a new, conflicting animation, it triggers a call to LayerAnimator::StopAnimating(). This results in LayerAnimator::FinishAnimation() being called for the active sequence.

FinishAnimation removes the sequence from the animator’s queue, transferring ownership into a local std::unique_ptr<LayerAnimationSequence> removed. When FinishAnimation returns, this local pointer goes out of scope, destroying the LayerAnimationSequence and its associated elements.

As the stack unwinds, execution returns to the while loop in LayerAnimationSequence::Progress(). Because there is no liveness check after the outcall, the function continues to execute lines 98-105 using the implicitly captured this pointer, which now points to freed memory.

Because the access is via the implicit this pointer rather than an explicit base::raw_ptr, BackupRefPtr (BRP) does not mitigate this UAF.

Potential Attacker Steps

Note: These are suggested steps; our tooling agent cannot dynamically run code to verify the full chain.

  1. Trigger UI Animation: An attacker in a compromised renderer process uses JavaScript/DOM manipulation to trigger a browser-process UI animation (e.g., rapidly resizing a window or triggering tab strip animations).
  2. Force Re-entrant Deletion: The attacker orchestrates the UI state so that a View observer synchronously stops the active animation in response to a bounds or transform update during an animation tick.
  3. Reclaim Memory: Concurrent with the animation tick, the attacker uses Mojo primitives (e.g., BlobRegistry or Data Pipes) to spray the browser process heap, attempting to allocate attacker-controlled data into the exact memory chunk freed by the sequence’s destruction.
  4. Control Flow Hijack: When LayerAnimationSequence::Progress() resumes, it reads from the attacker-controlled elements_ vector. On the next loop iteration, it attempts to call a virtual method (e.g., IsFinished or Start) on an element. This dereferences an attacker-supplied vtable pointer, leading to arbitrary code execution (a Sandbox Escape).

The call to ProgressToEnd() at line 96 should be guarded with a base::WeakPtr check. The developers already recognized this re-entrancy risk for the Progress() call at line 113 of the exact same function:

    base::WeakPtr<LayerAnimationSequence> alive(AsWeakPtr());
    if (elements_[current_index]->Progress(now, delegate))
      redraw_required = true;
    if (!alive)
      return;

A similar check must be applied around line 96. Additionally, the Start(...) call at line 110, as well as the element iteration loops in LayerAnimationSequence::ProgressToEnd() and LayerAnimationSequence::Abort(), lack WeakPtr checks and should be audited to prevent similar re-entrancy UAFs.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results 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