CVE-2026-11646
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/view_transition/view_transition_supplement.cc |
modified | |
forthird_party/blink/renderer/core/view_transition/view_transition_supplement.cc |
modified |
Files Changed
third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
Patch
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();
}
}
Original Bug Report
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 HTMLasan.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:
- 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. - Type confusion via fake Member<>: When the outer loop dereferences the reclaimed memory as a
Member<ViewTransition>, the attacker controls the vtable pointer. CallingOnCapturePhaseComplete()on a fake object gives arbitrary virtual call — a standard vtable hijack primitive. - 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)
Is the security issue related to a crash?
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