Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ViewTransitions
DescriptionUse after free in ViewTransitions
ComponentViewTransitions
Bug ClassUAF
Tracker517168239
Fix commite3c41472f7a7 (chromium/src) +3/-2
CISA KEVNot listed
CreditedQuac Tran
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
modified
for
third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
modified

Files Changed

  • third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
From e3c41472f7a775661fffabbbb71ca04c6793a555 Mon Sep 17 00:00:00 2001
From: Kevin Ellis <kevers@google.com>
Date: Thu, 28 May 2026 06:43:36 -0700
Subject: [PATCH] [vt] Fix use after capture in OnTransitionCaptured

Bug: 517168239
Change-Id: I2ae2b01ec9f87158cf4a4b604c4b901bc509e0c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7881960
Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
Reviewed-by: Vladimir Levin <vmpstr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637664}
---

diff --git a/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc b/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
index 7d09cff..d183e014 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
+++ b/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
@@ -351,10 +351,11 @@
   if (--in_flight_capture_requests_ == 0) {
     std::sort(captured_transitions_.begin(), captured_transitions_.end(),
               CompareTransitions);
-    for (auto captured_transition : captured_transitions_) {
+    HeapVector<Member<ViewTransition>> local_copy(captured_transitions_);
+    captured_transitions_.clear();
+    for (auto captured_transition : local_copy) {
       captured_transition->OnCapturePhaseComplete();
     }
-    captured_transitions_.clear();
   }
 }
 
Loading diff…

Original Bug Report

reported by tr...@gmail.com

use-after-poison in ViewTransitionSupplement::OnTransitionCaptured


Report description

use-after-poison in ViewTransitionSupplement::OnTransitionCaptured


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

Blink > ViewTransition


The problem

Please describe the technical details of the vulnerability

VULNERABILITY DETAILS

Type: use-after-poison (cppgc heap) / container invalidation during iteration Component: Blink > ViewTransition Affected file: third_party/blink/renderer/core/view_transition/view_transition_supplement.cc Affected function: ViewTransitionSupplement::OnTransitionCaptured (line 346-357) Version: Chromium 150.0.7847.0 (commit f3ab90db77d) Process: Renderer MiraclePtr: NOT PROTECTED (cppgc/Oilpan heap, not PartitionAlloc) User interaction: Print dialog appears; triggers regardless of user choice (cancel/close/print)

Variant of: The $50K LayerTreeHost::NotifyTransitionRequestsFinished UAF (fixed by CL 6542846). Same bug class — nested RunLoop re-entrancy causing container invalidation during iteration. The cc-level fix (PostTask) does not prevent the blink-level re-entrancy.

Root Cause Analysis

OnTransitionCaptured() iterates captured_transitions_ (a HeapVector<Member<ViewTransition>>) using a range-for loop. For each transition it calls OnCapturePhaseComplete(), which synchronously invokes a JavaScript callback via ViewTransition::ProcessCurrentState() -> DOMViewTransition::InvokeDOMChangeCallback() -> V8ViewTransitionCallback::Invoke().

If the JS callback triggers window.print(), the print preview path creates a nested RunLoop (base::RunLoop{kNestableTasksAllowed} at print_render_frame_helper.cc:2702). This nested RunLoop processes pending tasks, including compositor-originated BeginMainFrame tasks that run a full lifecycle update.

During the lifecycle update inside the nested RunLoop, newly created view transitions advance through their state machines (kCaptureTagDiscovery -> kCaptureRequestPending -> kCapturing), generating capture requests that are committed to the compositor. The compositor processes these immediately (due to kDelayLayerTreeViewDeletionOnLocalSwap, which is FEATURE_ENABLED_BY_DEFAULT) and posts completion callbacks back to the main thread.

These callbacks are processed during the same nested RunLoop, calling OnTransitionCaptured() re-entrantly. The re-entrant call executes captured_transitions_.clear() (line 356), which poisons the cppgc-managed HeapVector backing store. When the outer OnTransitionCaptured() resumes its range-for loop, it dereferences a Member<ViewTransition> pointer into poisoned memory.

The developers were aware of this re-entrancy risk. The same file contains ForEachTransition() (line 388-398) with the explicit comment: “Local copy of the list, since the function may modify the transition map.” However, OnTransitionCaptured() has no such protection.

Vulnerable Code

// view_transition_supplement.cc:346-357
void ViewTransitionSupplement::OnTransitionCaptured(ViewTransition* transition) {
  CHECK(transition);
  captured_transitions_.push_back(transition);           // [1]
  if (--in_flight_capture_requests_ == 0) {
    std::sort(captured_transitions_.begin(), ...);
    for (auto captured_transition : captured_transitions_) {  // [2] range-for
      captured_transition->OnCapturePhaseComplete();          // [3] -> JS -> print()
    }                                                         //     -> nested RunLoop
    captured_transitions_.clear();                            // [4] poisons memory
  }                                                           //     re-entrant [4]
}                                                             //     invalidates [2]

Compare with the protected pattern in the same file:

// view_transition_supplement.cc:388-398
void ViewTransitionSupplement::ForEachTransition(...) {
  // Local copy of the list, since the function may modify the transition map.
  HeapVector<Member<ViewTransition>> transitions;  // <-- local copy, SAFE
  // ... populate from element_transitions_ ...
  for (auto transition : transitions) {
    function(*transition);
  }
}

Reproduction Steps

chrome --no-sandbox --user-data-dir=/tmp/test poc.html

Close/cancel/interact with the print dialog that appears. The crash triggers regardless of user choice.

Note: --headless will NOT work. The nested RunLoop only exists in the print preview path (RequestPrintPreview), which is disabled when IsPrintPreviewEnabled() returns false in headless mode.

Trigger Chain

1. JS: 4x element.startViewTransition(callback)
   -> in_flight_capture_requests_ = 4
   -> 4 transitions enter kCapturing state

2. Compositor processes all 4 captures -> 4 PostTask'd callbacks fire sequentially:
   OnTransitionCaptured(A) -> push_back, counter 4->3
   OnTransitionCaptured(B) -> push_back, counter 3->2
   OnTransitionCaptured(C) -> push_back, counter 2->1
   OnTransitionCaptured(D) -> push_back, counter 1->0 -> ENTERS LOOP

3. Loop: OnCapturePhaseComplete(first) -> ProcessCurrentState() -> kCaptured
   -> InvokeDOMChangeCallback() -> V8 callback RUNS SYNCHRONOUSLY

4. JS callback:
   a. element5.startViewTransition(cb5)  -> in_flight_capture_requests_ = 1
      element6.startViewTransition(cb6)  -> in_flight_capture_requests_ = 2
   b. window.print()
      -> ScriptedPrint -> RequestPrintPreview(kScripted)
      -> base::RunLoop{kNestableTasksAllowed}.Run()  [line 2702]

5. DURING NESTED RUNLOOP:
   - BeginMainFrame -> lifecycle update -> RunViewTransitionStepsDuringMainFrame
   - Transitions E,F advance: kCaptureTagDiscovery -> kCapturing
   - PushPaintArtifactToCompositor -> capture requests committed
   - Impl processes captures immediately (kDelayLayerTreeViewDeletionOnLocalSwap)
   - PostTask chain: impl -> NotifyTransitionRequestsFinished [PostTask] -> callback

6. RE-ENTRANT OnTransitionCaptured(E): push_back(E), counter 2->1
   RE-ENTRANT OnTransitionCaptured(F): push_back(F), counter 1->0
   -> ENTERS INNER LOOP
   -> inner loop iterates captured_transitions_ [A,B,C,D,E,F]
   -> captured_transitions_.clear()  <<<--- POISONS MEMORY

7. Outer loop resumes -> dereferences Member<> into POISONED memory -> CRASH

Prerequisites (all enabled by default)

  • ScopedViewTransitions: runtime feature, status: "stable"
  • kDelayLayerTreeViewDeletionOnLocalSwap: FEATURE_ENABLED_BY_DEFAULT
  • No special flags required

ASAN Report

==424011==ERROR: AddressSanitizer: use-after-poison on address 0x7edc02142c2c
READ of size 4 at 0x7edc02142c2c thread T0 (chrome)
    #0 in blink::ViewTransitionSupplement::OnTransitionCaptured
       v8/include/cppgc/member.h:59:55

Address 0x7edc02142c2c is a wild pointer inside of access range of size 0x000000000004.
SUMMARY: AddressSanitizer: use-after-poison v8/include/cppgc/member.h:59:55
         in blink::ViewTransitionSupplement::OnTransitionCaptured

Shadow byte: f7 (Poisoned by user)

Task trace:
    #0 cc::LayerTreeHost::NotifyTransitionRequestsFinished  cc/trees/layer_tree_host.cc:605
    #1 cc::ProxyImpl::NotifyTransitionRequestFinished       cc/trees/proxy_impl.cc:648

Suggested Fix

Make a local copy before iterating, matching the existing ForEachTransition pattern:

void ViewTransitionSupplement::OnTransitionCaptured(ViewTransition* transition) {
  CHECK(transition);
  captured_transitions_.push_back(transition);
  if (--in_flight_capture_requests_ == 0) {
    std::sort(captured_transitions_.begin(), captured_transitions_.end(),
              CompareTransitions);
    // Local copy to prevent re-entrancy from invalidating the iteration.
    HeapVector<Member<ViewTransition>> local_copy(captured_transitions_);
    captured_transitions_.clear();
    for (auto captured_transition : local_copy) {
      captured_transition->OnCapturePhaseComplete();
    }
  }
}

Files

  • poc.html — Reproduction HTML
  • asan.txt — Full ASAN report from Chromium 150.0.7847.0

Impact analysis

Security Impact

Severity: High (Renderer Process Heap Corruption)

Primitive: use-after-poison on cppgc HeapVector backing store. After captured_transitions_.clear() poisons the memory, the outer loop dereferences Member<ViewTransition> pointers into attacker-influenced heap space. Because cppgc recycles backing store pages, a well-timed allocation between the inner clear() and the outer loop resumption can place attacker-controlled data at the poisoned address — converting the use-after-poison into a controlled read/write on a fake ViewTransition object.

MiraclePtr / BackupRefPtr: NOT PROTECTED. The HeapVector<Member<ViewTransition>> lives on the cppgc (Oilpan) managed heap, which is entirely outside PartitionAlloc’s MiraclePtr coverage. There is no dangling pointer detection or use-after-free mitigation for this object type.

Exploitation path:

  1. Heap spray during nested RunLoop: The nested RunLoop created by window.print() stays open for seconds (waiting for user interaction with print dialog). During this window, the attacker’s JS callback can allocate arbitrary objects (ArrayBuffers, strings, TypedArrays) via the same cppgc allocator to reclaim the freed backing store.
  2. Type confusion via fake Member<>: When the outer loop dereferences the reclaimed memory as a Member<ViewTransition>, the attacker controls the vtable pointer. Calling OnCapturePhaseComplete() on a fake object gives arbitrary virtual call — a standard vtable hijack primitive.
  3. RCE in renderer: Arbitrary virtual call → stack pivot → ROP/JOP chain → arbitrary code execution within the renderer process sandbox.

Attack surface:

  • Triggered from any web page via standard DOM APIs (element.startViewTransition() + window.print())
  • No special permissions, flags, or user gestures required beyond the print dialog appearing
  • All prerequisites enabled by default in stable Chrome (ScopedViewTransitions: "stable", kDelayLayerTreeViewDeletionOnLocalSwap: FEATURE_ENABLED_BY_DEFAULT)
  • Works on all desktop platforms with print preview (Linux, Windows, macOS, ChromeOS)

User interaction: Minimal. The window.print() call opens a print dialog, but the UAF triggers regardless of user choice (cancel, close, or print). A phishing page could social-engineer the user with “Please print this receipt” to make the dialog appear expected. The corruption occurs during the nested RunLoop — before the user interacts with the dialog.

Scope: Renderer process only. Full exploitation requires combining with a sandbox escape for system-level impact. However, renderer-process RCE alone enables:

  • Reading all cross-site data in the renderer (cookies, passwords in autofill, DOM content)
  • Bypassing Site Isolation within the compromised renderer
  • Serving as the first stage of a full exploit chain (renderer RCE → sandbox escape → system compromise)

The cause

What version of Chrome have you found the security issue in?

Chromium 150.0.7847.0 (commit f3ab90db77d)

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption

How would you like to be publicly acknowledged for your report?

Quac Tran

View on issue tracker