CVE-2025-3066
Overview
Background
- `NavigationThrottle`
- a browser-process hook that can defer, resume, or cancel a navigation;
Resume()continues a navigation that was previously deferred. - `SubframeHistoryNavigationThrottle`
- a throttle used during subframe history (back/forward) navigations under Site Isolation, tracking progress in its
state_member. - `state_` / `State`
- the throttle’s lifecycle field, transitioning among values including
State::kDeferredandState::kRunningAfterResumeSignal. - Use-after-free (UAF)
- dereferencing memory after it has been freed; here, touching the throttle instance after
Resume()has synchronously destroyed it.
Root Cause Analysis
In SubframeHistoryNavigationThrottle::Resume(), the pre-patch code called NavigationThrottle::Resume() while state_ == State::kDeferred was true, and only afterward assigned state_ = State::kRunningAfterResumeSignal. The problem is that NavigationThrottle::Resume() can synchronously delete the throttle instance (it can drive the navigation to completion and tear down the throttle), so the subsequent write to state_ dereferences freed memory — a use-after-free. The commit message and the new comment make this explicit: “Resume() can synchronously delete this navigation throttle, so no code after this call should reference the throttle instance.”
The fix reorders the operations: it captures const bool should_resume = state_ == State::kDeferred;, updates state_ = State::kRunningAfterResumeSignal first while the object is still alive, and only then conditionally calls NavigationThrottle::Resume(), with no member access afterward. This removes the dangling write and preserves the intended state transition.
state_ after calling NavigationThrottle::Resume(), which can synchronously free the throttle; the fix updates state_ before the potentially self-destroying Resume() call and touches no members after it.Attack Path
- Defer a subframe history navigation
A page triggers a subframe back/forward navigation that causes
SubframeHistoryNavigationThrottleto enterState::kDeferred. - Signal resume
The navigation is resumed, invoking
SubframeHistoryNavigationThrottle::Resume()withstate_ == State::kDeferredtrue. - Synchronous self-deletion
NavigationThrottle::Resume()synchronously completes/tears down the navigation and deletes the throttle instance while still inside theResume()call. - Use-after-free write
Control returns to the pre-patch code, which writes
state_ = State::kRunningAfterResumeSignalon the now-freed throttle, dereferencing freed memory; further exploitation (controlling the freed allocation for a read/write primitive) is(inferred).
Impact Assessment
SubframeHistoryNavigationThrottle object in the browser process, a use-after-free per the advisory’s UAF classification for Site Isolation. Because it occurs in the browser process, a corrupted allocation could be leveraged toward memory corruption, but any progression to a controlled read/write or code execution is (inferred). The advisory reports this as a use-after-free and does not state in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/subframe_history_navigation_throttle.cc |
modified |
Files Changed
content/browser/renderer_host/subframe_history_navigation_throttle.cc
Audit Directions
- Member access after resume/completeAudit throttles, callbacks, and observers that touch
thisor member state after invoking a resume/complete/continue method that can synchronously destroy the object. - Order of state update vs. reentrant callLook for the pattern of calling a base-class or delegate method that may free the object before finishing local bookkeeping; state should be committed before any potentially self-destroying call.
- Navigation throttle lifetimesReview other
NavigationThrottlesubclasses’Resume/Cancelimplementations for the same synchronous-deletion hazard aroundNavigationThrottle::Resume()/CancelDeferredNavigation().
Patch
From 94ec04ec6bd89868fe9508a76d058243db8f0623 Mon Sep 17 00:00:00 2001 From: Takashi Toyoshima <[email protected]> Date: Mon, 24 Mar 2025 23:28:14 -0700 Subject: [PATCH] SubframeHistoryNavigationThrottle: Update the state_ first It's nice to update the `state_` before calling Resume() to avoid an ordering issue. Bug: 405140652 Change-Id: Ife607f8121eadcf30f86c5478a688bd7e61bccb1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6387055 Commit-Queue: Takashi Toyoshima <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Cr-Commit-Position: refs/heads/main@{#1437323} --- diff --git a/content/browser/renderer_host/subframe_history_navigation_throttle.cc b/content/browser/renderer_host/subframe_history_navigation_throttle.cc index 85377dac..2153c983 100644 --- a/content/browser/renderer_host/subframe_history_navigation_throttle.cc +++ b/content/browser/renderer_host/subframe_history_navigation_throttle.cc @@ -44,10 +44,13 @@ } void SubframeHistoryNavigationThrottle::Resume() { - if (state_ == State::kDeferred) { - NavigationThrottle::Resume(); - } + const bool should_resume = state_ == State::kDeferred; state_ = State::kRunningAfterResumeSignal; + if (should_resume) { + NavigationThrottle::Resume(); + // `Resume()` can synchronously delete this navigation throttle, so no code + // after this call should reference the throttle instance. + } } void SubframeHistoryNavigationThrottle::Cancel() {
Original Bug Report
UAF when accessing member variable after destruction of throttle (SubframeHistoryNavigationThrottle)
Steps to reproduce the problem
I will provide a reproduction of the UAF soon. Bear with me, please. Though I think the UAF is trivial.
Problem Description
A SubframeHistoryNavigationThrottle is intended to defer subframe history navigations while the main frame commits main-frame same-document history navigations. When the Resume() method is called, it triggers a call to resume the current throttle 0. However, the call to NavigationThrottle::Resume() can synchronously delete this. This leads to a use-after-free when subsequently accessing the member variable state_ 1.
void SubframeHistoryNavigationThrottle::Resume() {
if (state_ == State::kDeferred) {
NavigationThrottle::Resume(); [0]
}
state_ = State::kRunningAfterResumeSignal; [1]
}
This issue resembles crbug.com/40063127.
Suggested Fix: Move the member variable assignment (state_ = State::kRunningAfterResumeSignal;) to before calling Resume():
void SubframeHistoryNavigationThrottle::Resume() {
state_ = State::kRunningAfterResumeSignal;
if (state_ == State::kDeferred) {
NavigationThrottle::Resume();
// `Resume()` can synchronously delete this navigation throttle, so no code
// after this call should reference the throttle instance.
}
}
Additional Comments
PoC soon.
Summary
UAF when accessing member variable after destruction of throttle (SubframeHistoryNavigationThrottle)
Custom Questions
Type of crash:
browser
Reporter credit:
Sven Dysthe @svn_dy
Additional Data
Category: Security
Chrome Channel: Canary
Regression: N/A
- https://crbug.com/40063127
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/subframe_history_navigation_throttle.cc;drc=1563c4e1a33c72f24006ce2ee28eea7629632370;l=48
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/subframe_history_navigation_throttle.cc;drc=1563c4e1a33c72f24006ce2ee28eea7629632370;l=50