Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Tracker501524262
Fix commit6eb376c4e511 (chromium/src) +85/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

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
From 6eb376c4e511ff1e8ca1b802cec5cbbb5df7ab85 Mon Sep 17 00:00:00 2001
From: Tzarial <[email protected]>
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 <[email protected]>
Reviewed-by: Jonathan Ross <[email protected]>
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 [email protected]

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