CVE-2026-9993
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/base/interaction/element_tracker.cc |
modified |
Files Changed
ui/base/interaction/element_tracker.cc
Patch
From d35c1b1b985ea1f464b496b46b348808e32153c3 Mon Sep 17 00:00:00 2001 From: Dana Fried <[email protected]> Date: Tue, 19 May 2026 09:17:53 -0700 Subject: [PATCH] [Interaction] Use raw_ptr during all callbacks This prevents a possible (but highly unlikely) UAF during certain operations via a hard crash, which should indicate to the calling code that it is doing something unsafe with the API. Note that since CallbackList::Notify() is a variadic template function, the raw_ptr is not unwrapped during that call, including the internal loop over callbacks. Bug: 513208588 Change-Id: I1833009714fb6bf782d0cee79be3160b0b3db4f4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7857060 Commit-Queue: Dana Fried <[email protected]> Commit-Queue: Foromo Daniel Soromou <[email protected]> Reviewed-by: Foromo Daniel Soromou <[email protected]> Cr-Commit-Position: refs/heads/main@{#1632899} --- diff --git a/ui/base/interaction/element_tracker.cc b/ui/base/interaction/element_tracker.cc index 1f8f6b327..390a9df 100644 --- a/ui/base/interaction/element_tracker.cc +++ b/ui/base/interaction/element_tracker.cc @@ -93,7 +93,8 @@ return custom_event_callbacks_.Add(callback); } - void NotifyElementShown(raw_ptr<TrackedElement, CtnExperimental>& element) { + void NotifyElementShown( + const raw_ptr<TrackedElement, CtnExperimental>& element) { DCHECK(element); DCHECK_EQ(identifier(), Unwrap(element->identifier())); // Zero context data is the "all contexts" entry and doesn't actually store @@ -109,13 +110,14 @@ } void NotifyElementActivated( - raw_ptr<TrackedElement, CtnExperimental>& element) { + const raw_ptr<TrackedElement, CtnExperimental>& element) { // Note: "All contexts" does not require the element to be present here. DCHECK(!context_ || element_lookup_.contains(element)); activated_callbacks_.Notify(element); } - void NotifyElementHidden(TrackedElement* element) { + void NotifyElementHidden( + const raw_ptr<TrackedElement, CtnExperimental>& element) { if (context_) { const auto it = element_lookup_.find(element); CHECK(it != element_lookup_.end()); @@ -125,7 +127,8 @@ hidden_callbacks_.Notify(element); } - void NotifyCustomEvent(TrackedElement* element) { + void NotifyCustomEvent( + const raw_ptr<TrackedElement, CtnExperimental>& element) { custom_event_callbacks_.Notify(element); }
Original Bug Report
Potential Browser-process UAF in ElementTracker::NotifyCustomEvent due to pointer snapshotting
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 https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free vulnerability exists in ui::ElementTracker where custom event notifications snapshot element pointers as bare pointers. This allows re-entrant destruction of an element to leave dangling pointers in active notification loops, bypassing internal safeguards and MiraclePtr mitigations.
Affected files:
ui/base/interaction/element_tracker.ccui/base/interaction/interaction_sequence.cccomponents/user_education/webui/help_bubble_handler.cc
Estimated timestamp from git blame: 2023-06-24
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in the ui::ElementTracker component. The issue stems from an inconsistency in how different event types handle element pointers during the notification process. While NotifyElementShown and NotifyElementActivated correctly pass references to raw_ptr to handle re-entrancy, NotifyCustomEvent snapshots the pointer as a bare C++ pointer before entering the notification loop. If a callback in the loop synchronously destroys the element, subsequent callbacks will be invoked with a dangling pointer.
Root Cause Analysis
ui::ElementTracker uses a notification_elements_ list of raw_ptr<TrackedElement> to protect elements currently undergoing notification. When NotifyElementHidden is called (due to an element being destroyed or hidden), it nulls out any matching entries in this list to prevent subsequent callbacks in an active loop from using the freed element.
In ui/base/interaction/element_tracker.cc, the methods for ‘Shown’ and ‘Activated’ events pass the element as a reference to a raw_ptr:
void NotifyElementShown(raw_ptr<TrackedElement, CtnExperimental>& element) {
shown_callbacks_.Notify(element);
}
Because base::RepeatingCallbackList::Notify forwards this reference, the pointer is re-evaluated for each callback. If a re-entrant call nulls the raw_ptr, the remaining callbacks receive nullptr safely.
However, ElementData::NotifyCustomEvent (line 128) takes the pointer by value as a bare TrackedElement*:
void NotifyCustomEvent(TrackedElement* element) {
custom_event_callbacks_.Notify(element);
}
When called from ElementTracker::NotifyCustomEvent (line 537), the raw_ptr& is converted to a bare pointer. This snapshots the address. If an early callback in the Notify() loop destroys the element, the re-entrancy protection in NotifyElementHidden will null the raw_ptr entry, but the local element variable in the Notify loop remains pointing to the now-freed memory.
Notably, since the raw_ptr is nulled before the object is actually freed, MiraclePtr (BackupRefPtr) may not quarantine the memory, as it believes no protected references remain.
Potential Impact and Reachability
An attacker who has compromised a renderer process (specifically the PDF extension renderer) can reach this logic via the PdfHelpBubbleHandlerFactory Mojo interface. By triggering a custom event on a TrackedElement and arranging for multiple subscribers (e.g., via an InteractionSequence or global listeners like CriticalUserJourneyService), the attacker can potentially cause a Use-After-Free in the unsandboxed browser process.
Dereferencing this dangling pointer—for instance, in the FilterCallback wrapper or during virtual method dispatch—could lead to arbitrary code execution in the browser process.
Suggested Attack Steps (Potential)
- From a compromised PDF-extension renderer, bind
help_bubble::mojom::PdfHelpBubbleHandlerFactoryand callCreateHelpBubbleHandler. - Use
TrackedElementVisibilityChangedto register an anchor element (e.g.,kPdfInkSignaturesDrawElementId). - Arrange for two or more subscribers to a custom event for that element.
- Trigger the custom event via
TrackedElementCustomEventin the Mojo interface. - Ensure the first subscriber’s callback synchronously destroys the element (e.g., by closing the tab or navigating).
- The second subscriber will receive the snapshotted dangling pointer.
Recommended Fix
Change the signature of ElementData::NotifyCustomEvent to accept a reference to the raw_ptr instead of a bare pointer, ensuring it matches the pattern used by NotifyElementShown and NotifyElementActivated:
void NotifyCustomEvent(raw_ptr<TrackedElement, CtnExperimental>& element);
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.