CVE-2026-10885
Overview
Background
- `CWVWebView`
- The Objective-C++ wrapper class in Chrome for iOS (
web_view) that owns aweb::WebState(_webState) and exposes web-content APIs to embedders. - `_backForwardList` / `navigationManager`
CWVBackForwardListexposes navigation history; itsnavigationManagerproperty points into state owned by the_webStateand is unsafe to use once the web state is torn down.- `shutDown` vs `dealloc`
shutDownis an explicit teardown entry point an embedder may call;deallocis the Objective-C destructor that always runs, and the two are independent teardown paths that may each run alone.- `WebViewHolder` / `_webStateObserver`
- Helper that associates a
CWVWebViewwith itsweb::WebStateand the observer registered on that web state; both must be detached during cleanup.
Root Cause Analysis
Teardown of a CWVWebView could occur via two independent paths — shutDown and dealloc — but only shutDown reset _backForwardList.navigationManager = nil; the old dealloc merely removed the web-state observer and called WebViewHolder::RemoveFromWebState without clearing the back-forward list’s manager pointer. Because shutDown “may not be called before dealloc” (per the commit), an object destroyed directly through dealloc would leave _backForwardList holding a navigationManager pointer into state that is destroyed with the web state, creating a dangling pointer and a use-after-free when the back-forward list is subsequently touched.
The fix extracts the shared teardown into a new cleanupWebStateReference helper that always nils _backForwardList.navigationManager, removes the observer, and calls WebViewHolder::RemoveFromWebState, and invokes it from both shutDown and dealloc, so the “CWVBackForwardList is unsafe to use after shutting down” invariant now holds on every destruction path. The change is purely additive cleanup consolidation — it tightens teardown by ensuring the navigationManager reset is no longer exclusive to shutDown.
navigationManager nil-out (and the rest of the cleanup) lived only on the shutDown path, so a dealloc-only destruction left _backForwardList with a dangling pointer; the fix centralizes cleanup in cleanupWebStateReference and calls it from both destruction paths.Attack Path
- Create a web view
Content (or an embedder driven by content) causes a
CWVWebViewwith a live_webStateand a populatedCWVBackForwardListto exist. - Destroy without `shutDown`
The
CWVWebViewis released anddeallocruns without a priorshutDown, so the old code frees the web state but leaves_backForwardList.navigationManagerpointing at freed state. - Use the stale back-forward list
A subsequent access to
_backForwardList/navigationManagerdereferences the dangling pointer, causing a use-after-free. - Downstream corruption
Exploiting the freed navigation-manager memory toward a read/write primitive or code execution is
(inferred)and not demonstrated by the diff.
Impact Assessment
CWVWebView is torn down through dealloc without shutDown, leaving a dangling navigationManager in _backForwardList. The vendor advisory rates this critical; it does not report in-the-wild exploitation, and any escalation beyond the dangling-pointer condition is (inferred). Reachability depends on the destruction ordering of the web view rather than a special flag or gesture, but the exact trigger that forces dealloc ahead of shutDown is not shown in the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/web_view/internal/cwv_web_view.mm |
modified |
Files Changed
ios/web_view/internal/cwv_web_view.mm
Audit Directions
- Divergent destruction pathsAudit classes with both an explicit
shutDown/closemethod and adealloc/destructor for cleanup steps present on one path but missing on the other. - Pointers into owned-but-separately-destroyed stateReview properties like
navigationManagerthat alias into a member’s storage (_webState) and must be nilled before or with that member’s teardown. - Observer and holder deregistrationCheck every teardown path removes registered observers and association holders (
RemoveObserver,WebViewHolder::RemoveFromWebState) exactly once and unconditionally.
Patch
From 67184bab7ef9a8e301189d8a9dcd78d82393f27c Mon Sep 17 00:00:00 2001 From: Mike Dougherty <[email protected]> Date: Wed, 22 Apr 2026 17:33:44 -0700 Subject: [PATCH] Ensure CWVWebView cleanup is consistent `shutdown` may not be called before `dealloc`. In these cases, ensure that `_backForwardList.navigationManager` is set to nil. Additionally, move the dealloc and shutdown methods to be close to each other and move shared logic to a helper method to make it more clear that there are two destrution paths. Fixed: 504072665 Change-Id: I352c3f75de196f7136046bcc74c908ea42d95b94 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7783652 Reviewed-by: Rohit Rao <[email protected]> Commit-Queue: Mike Dougherty <[email protected]> Cr-Commit-Position: refs/heads/main@{#1619215} --- diff --git a/ios/web_view/internal/cwv_web_view.mm b/ios/web_view/internal/cwv_web_view.mm index 29b4ba1..8377e03 100644 --- a/ios/web_view/internal/cwv_web_view.mm +++ b/ios/web_view/internal/cwv_web_view.mm @@ -511,16 +511,6 @@ _webState->GetWebViewProxy().allowsLinkPreview = allowsLinkPreview; } -- (void)dealloc { - if (_webState) { - if (_webStateObserver) { - _webState->RemoveObserver(_webStateObserver.get()); - _webStateObserver.reset(); - } - WebViewHolder::RemoveFromWebState(_webState.get()); - } -} - - (void)goBack { if (![self isWebStateSafeToUse]) { return; @@ -1253,20 +1243,31 @@ - (void)shutDown { if (_webState) { - // CWVBackForwardList is unsafe to use after shutting down. - _backForwardList.navigationManager = nil; - // To handle the case where -[CWVWebView encodeRestorableStateWithCoder:] is // called after this method, precompute the session storage so it may be // used during encoding later. [_serializationHelper updateStateFromWebState:_webState.get()]; - if (_webStateObserver) { - _webState->RemoveObserver(_webStateObserver.get()); - _webStateObserver.reset(); - } - WebViewHolder::RemoveFromWebState(_webState.get()); + + [self cleanupWebStateReference]; _webState.reset(); } } +- (void)dealloc { + if (_webState) { + [self cleanupWebStateReference]; + } +} + +- (void)cleanupWebStateReference { + // CWVBackForwardList is unsafe to use after shutting down. + _backForwardList.navigationManager = nil; + + if (_webStateObserver) { + _webState->RemoveObserver(_webStateObserver.get()); + _webStateObserver.reset(); + } + WebViewHolder::RemoveFromWebState(_webState.get()); +} + @end
Original Bug Report
Potential Use-After-Free in CWVBackForwardList via dangling NavigationManager
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. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A Use-After-Free vulnerability exists in ios/web_view where CWVBackForwardList retains a dangling raw C++ pointer to a destroyed web::NavigationManager. This occurs when an embedding application releases a CWVWebView without calling the internal shutDown method, leaving the pointer un-nulled during standard deallocation. Accessing the list thereafter results in virtual calls on freed memory, potentially leading to Remote Code Execution.
Affected files:
ios/web_view/internal/cwv_web_view.mmios/web_view/internal/cwv_back_forward_list.mmios/web_view/internal/cwv_back_forward_list_internal.h
Estimated timestamp from git blame: 2018-10-24
Background
In ChromeWebView.framework (ios/web_view), the CWVBackForwardList Objective-C object provides the host application with access to the navigation history of a CWVWebView. Internally, it relies on a raw C++ pointer to fetch data, declared in ios/web_view/internal/cwv_back_forward_list_internal.h:
@property(nonatomic, nullable) const web::NavigationManager* navigationManager;
Vulnerability Details
The web::NavigationManager memory is owned by the web::WebState instance managed by the parent CWVWebView. A Use-After-Free condition arises because the standard deallocation path of CWVWebView does not clear the navigationManager pointer stored in its associated CWVBackForwardList.
While the internal -[CWVWebView shutDown] method correctly nullifies this pointer (_backForwardList.navigationManager = nil;), standard ARC deallocation via -[CWVWebView dealloc] does not. It is a documented expectation that dealloc can be called without shutDown occurring first (see comments in ios/web_view/internal/autofill/web_view_autofill_client_ios.h).
When a CWVWebView is deallocated, its std::unique_ptr<web::WebState> is destroyed, which in turn destroys the NavigationManagerImpl. If the embedding iOS application has retained a strong reference to the CWVBackForwardList object to power its history UI, the navigationManager property becomes a dangling pointer to freed memory.
Subsequent access to the list (e.g., -[CWVBackForwardList currentItem]) bypasses the nil check because the pointer is dangling, and executes a virtual C++ call: self.navigationManager->GetLastCommittedItem().
Potential Exploitation Steps
Note: These are theoretical steps based on code analysis; our tooling agent does not run exploit code.
- Setup: An attacker hosts a malicious page and the victim visits it via an app using
ChromeWebView.framework. - Popup Creation: The attacker’s script calls
window.open(), prompting the embedder to create a newCWVWebViewinstance. - UI Retention: The embedder application requests
webView.backForwardListand retains it for its custom navigation UI. - Trigger Closure: The attacker’s script calls
window.close()on the popup. This triggers-[CWVUIDelegate webViewDidClose:]. - Deallocation: The embedder handles the delegate call by removing the popup from the view hierarchy and dropping its strong reference. The
CWVWebViewdeallocates, destroying theNavigationManagerwhile leaving the dangling pointer in the retainedCWVBackForwardList. - Heap Spray: The attacker’s surviving opener page runs a JavaScript loop to allocate large objects, reclaiming the freed memory slot previously held by the
NavigationManagerImpl. (Note:ios/web_viewapp extensions are built withuse_partition_alloc = false, meaning BackupRefPtr mitigation is not active). - Execution: The embedder app updates its UI and accesses the
currentItemof the retained back-forward list. The virtual call uses the attacker-controlled vtable, leading to Remote Code Execution (RCE) in the browser process.
Suggested Fix
The -[CWVWebView dealloc] method should be updated to explicitly nullify the navigationManager pointer on the _backForwardList, mirroring the logic found in the shutDown method.
// In ios/web_view/internal/cwv_web_view.mm
- (void)dealloc {
_backForwardList.navigationManager = nil;
if (_webState) {
if (_webStateObserver) {
_webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
}
WebViewHolder::RemoveFromWebState(_webState.get());
}
}
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
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.