CVE-2026-7361
Overview
Background
- `WebStateImpl::RealizedWebState`
- the iOS
//ios/webobject that backs a realized tab/web page and fans navigation lifecycle callbacks out to its registered observers. - `NavigationContextImpl`
- the per-navigation context object passed to observers describing an in-flight navigation (redirect, finish, same-document, etc.).
- Observer reentrancy
- an observer notified inside an
observers()loop can cancel the navigation, which synchronously destroys theNavigationContextImplwhile the loop is still iterating and still holds a rawcontextpointer. - `kDetectDestroyedNavigationContexts`
- the feature flag that gates the new liveness check, so the guard can be rolled out behind
base::FeatureList::IsEnabled(...).
Root Cause Analysis
OnNavigationRedirected and the DidFinishNavigation dispatch loop iterated over observers() and called observer.DidRedirectNavigation(owner_, context) / observer.DidFinishNavigation(owner_, context) with a raw NavigationContextImpl* context. An observer invoked during that loop could cancel the navigation, which destroys the NavigationContextImpl; the loop then continued and passed the now-freed context to the remaining observers, a use-after-free. The finish path additionally read context->IsSameDocument() after the loop, another post-free dereference.
The fix captures base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr() before the loop and breaks out as soon as !weak_context (gated by the kDetectDestroyedNavigationContexts feature), and it snapshots const bool same_document = context->IsSameDocument() before the loop so the later favicon logic uses the cached value instead of touching a possibly-freed object.
context pointer that an observer could free mid-iteration; the fix watches the context with a WeakPtr and breaks the loop once it is destroyed, and caches IsSameDocument() beforehand so no access occurs after the free.Attack Path
- Start a navigation that redirects or finishes
Web content in a tab drives a navigation through the redirect or finish lifecycle on iOS, entering the affected
RealizedWebStatedispatch loop. - Cancel during observer notification
One of the notified observers cancels the navigation in response, synchronously destroying the
NavigationContextImpl. (inferred) - Continue iterating on freed context
Pre-patch, the loop proceeds to the next observer and hands it the freed
context(and the finish path later readscontext->IsSameDocument()), dereferencing freed memory — the use-after-free. (inferred)
Impact Assessment
//ios/web), reachable from ordinary navigation handling on iOS Chrome when an observer cancels a navigation during redirect/finish dispatch. An attacker who arranges that cancellation operates on a freed NavigationContextImpl passed to subsequent observers, potentially corrupting memory (inferred); note the guard ships behind the kDetectDestroyedNavigationContexts flag, so protection depends on that feature being enabled. The advisory does not report in-the-wild exploitation, and any escalation beyond the freed-object access is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/web/web_state/web_state_impl_realized_web_state.mm |
modified |
Files Changed
ios/web/web_state/web_state_impl_realized_web_state.mm
Audit Directions
- Observer loops holding raw pointersAudit every
for (auto& observer : observers())dispatch that passes a raw navigation/context/document pointer, since any observer can reentrantly destroy that object; prefer aWeakPtrguard that breaks the loop. - Post-loop dereferencesFlag reads of the dispatched object after the observer loop (like the original
context->IsSameDocument()), and cache needed values before notifying observers. - Feature-flag-gated safety checksReview guards added behind flags such as
kDetectDestroyedNavigationContextsto confirm the flag’s default and that the unflagged path is not still exploitable.
Patch
From 2ed116751ff0f0b4f8d67f3298ceae16143a492b Mon Sep 17 00:00:00 2001 From: Mike Dougherty <[email protected]> Date: Fri, 17 Apr 2026 14:07:12 -0700 Subject: [PATCH] Break from navigation observer loops if WebState is deallocated Fixed: 493221953 Change-Id: I02ecc528453f8d7895f5f9bddb39d05d04ecf748 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7766683 Reviewed-by: Andrew Liu <[email protected]> Auto-Submit: Mike Dougherty <[email protected]> Commit-Queue: Andrew Liu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1616847} --- diff --git a/ios/web/web_state/web_state_impl_realized_web_state.mm b/ios/web/web_state/web_state_impl_realized_web_state.mm index c56ff4c..2a1de50 100644 --- a/ios/web/web_state/web_state_impl_realized_web_state.mm +++ b/ios/web/web_state/web_state_impl_realized_web_state.mm @@ -296,7 +296,14 @@ void WebStateImpl::RealizedWebState::OnNavigationRedirected( NavigationContextImpl* context) { + base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr(); for (auto& observer : observers()) { + // Observers might cancel this navigation, destroying the context. Guard + // against that by checking if the context is still alive. + if (!weak_context && base::FeatureList::IsEnabled( + features::kDetectDestroyedNavigationContexts)) { + break; + } observer.DidRedirectNavigation(owner_, context); } } @@ -309,12 +316,20 @@ return; } + const bool same_document = context->IsSameDocument(); + base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr(); for (auto& observer : observers()) { + // Observers might cancel this navigation, destroying the context. Guard + // against that by checking if the context is still alive. + if (!weak_context && base::FeatureList::IsEnabled( + features::kDetectDestroyedNavigationContexts)) { + break; + } observer.DidFinishNavigation(owner_, context); } // Update cached_favicon_urls_. - if (!context->IsSameDocument()) { + if (!same_document) { // Favicons are not valid after document change. Favicon URLs will be // refetched by CRWWebController and passed to OnFaviconUrlUpdated. cached_favicon_urls_.clear();
Original Bug Report
Potential Use-After-Free in RealizedWebState via synchronous NavigationContext destruction
Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.
Overview: A potential Use-After-Free (UAF) exists in WebStateImpl::RealizedWebState when notifying observers of navigation events. If an observer synchronously destroys the NavigationContextImpl (e.g., by disabling web usage), subsequent observers and method accesses use a dangling raw pointer. While OnNavigationStarted mitigates this with a base::WeakPtr check, OnNavigationRedirected and OnNavigationFinished lack this protection, potentially leading to Remote Code Execution (RCE).
Affected files:
ios/web/web_state/web_state_impl_realized_web_state.mm
Estimated timestamp from git blame: 2025-01-23
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in ios/web/web_state/web_state_impl_realized_web_state.mm. When notifying WebStateObservers of navigation events (DidRedirectNavigation, DidFinishNavigation), the code passes a raw pointer to a NavigationContextImpl object. If an observer synchronously triggers an action that causes this context to be destroyed, subsequent access to the pointer results in a UAF.
Technical Details
In WebStateImpl::RealizedWebState, the following methods iterate over observers using a raw NavigationContextImpl* pointer:
-
OnNavigationStarted(NavigationContextImpl* context)(line 272): This method contains a mitigation usingbase::WeakPtr(line 284), gated behind thefeatures::kDetectDestroyedNavigationContextsfeature flag (line 288). If the context is destroyed during the loop, the loop breaks. -
OnNavigationRedirected(NavigationContextImpl* context)(line 296): This method iterates through observers and callsDidRedirectNavigationwithout any checks for the validity of the context between calls. If an observer causes the navigation to be canceled or the web view to be removed, the next observer receives a dangling pointer. -
OnNavigationFinished(NavigationContextImpl* context)(line 303): Similar to the above, it callsDidFinishNavigationwithout aWeakPtrcheck. Crucially, it also accessescontext->IsSameDocument()on line 316 after the observer notification loop has completed.IsSameDocument()is a virtual method. If any observer caused the context’s destruction during the loop, this virtual method call on a freed object makes RCE a viable threat.
Potential Trigger Mechanism
NavigationContextImpl objects for pending navigations (including those in the REDIRECTED state or downloads) are owned by CRWWKNavigationStates via a std::unique_ptr. A synchronous destruction of these objects can be triggered by calling WebState::SetWebUsageEnabled(false), which eventually invokes CRWWebController removeWebView.
Here is a potential step-by-step sequence an attacker might use to trigger this vulnerability:
- Initiation: An attacker-controlled web page initiates a navigation (e.g., triggering a server-side redirect or a download).
- State Update:
CRWWKNavigationHandlerupdates the state of the navigation inCRWWKNavigationStatestoREDIRECTEDor leaves it pending. - Observer Notification: The handler invokes
self.webStateImpl->OnNavigationRedirected(context)orself.webStateImpl->OnNavigationFinished(context), passing the raw pointer toWebStateImpl. - Looping Observers: Execution enters
WebStateImpl::RealizedWebStateand begins iterating overWebStateObserverobjects. - Synchronous Destruction: During this iteration, an observer synchronously triggers an action that disables web usage for the
WebState(e.g., viaWebUsageEnablerBrowserAgent::SetWebUsageEnabled(false)). This calls[web_controller_ setWebUsageEnabled:NO], which calls[self removeWebView]. - Pending Navigations Cleared: Inside
removeWebView, the code explicitly iterates over all pending navigations and calls[self.navigationHandler.navigationStates removeNavigation:navigation]. - Memory Freed:
[CRWWKNavigationStates removeNavigation:]removes the navigation record and releases itsstd::unique_ptr<web::NavigationContextImpl>. The context is destroyed and freed. - Use-After-Free: Execution returns to the observer loop in
WebStateImpl::RealizedWebState. Thecontextparameter is now a dangling pointer. - Subsequent Access: The loop continues to the next observer, passing the dangling pointer. Furthermore, if this occurred in
OnNavigationFinished, the code evaluatescontext->IsSameDocument()on the freed object. BecauseIsSameDocument()is virtual, this dereferences a hijacked vtable pointer.
Note: The LLM agent does not have the ability to run code or build a working PoC, so these steps are suggested/potential based on static analysis.
Impact
As ios/web runs within the main browser process on iOS, this Use-After-Free vulnerability allows for memory corruption. Because it involves a virtual method call (IsSameDocument()) on a synchronously freed object, an attacker could potentially overwrite the memory via heap spraying and hijack the control flow, leading to full Remote Code Execution (RCE) and a sandbox escape.
Evaluated with Chrome root at commit: 43c4f3945742db6f06efbaf9a77b90f34a720277
Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.