CVE-2026-9948
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/base/cocoa/tool_tip_base_view.mm |
modified | |
forui/base/cocoa/tool_tip_base_view.mm |
modified |
Files Changed
ui/base/cocoa/tool_tip_base_view.mm
Patch
From c24bb170bc654b5db62de638e93a37dc38a900c0 Mon Sep 17 00:00:00 2001
From: Avi Drissman <avi@chromium.org>
Date: Tue, 19 May 2026 14:25:54 -0700
Subject: [PATCH] Clear danging pointers
When a tool tip is detached, clear out _trackingRectUserData.
Fixed: 503790201
Change-Id: I1d24cf8bd81cdb390397903f8df76e596a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858673
Reviewed-by: Bryan Oltman <bryanoltman@google.com>
Auto-Submit: Avi Drissman <avi@chromium.org>
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Cr-Commit-Position: refs/heads/main@{#1633109}
---
diff --git a/ui/base/cocoa/tool_tip_base_view.mm b/ui/base/cocoa/tool_tip_base_view.mm
index dfe52a1a..a041eb2 100644
--- a/ui/base/cocoa/tool_tip_base_view.mm
+++ b/ui/base/cocoa/tool_tip_base_view.mm
@@ -5,11 +5,13 @@
#import "ui/base/cocoa/tool_tip_base_view.h"
#include "base/check.h"
+#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
// Below is the nasty tooltip stuff -- copied from WebKit's WebHTMLView.mm
+// https://github.com/WebKit/WebKit/blob/main/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm
// with minor modifications for code style and commenting.
//
// The 'public' interface is -setToolTipAtMousePoint:. This differs from
@@ -54,7 +56,7 @@
@implementation ToolTipBaseView {
// These are part of the magic tooltip code from WebKit's WebHTMLView:
id __weak _trackingRectOwner;
- raw_ptr<void, DanglingUntriaged> _trackingRectUserData;
+ raw_ptr<void> _trackingRectUserData;
NSTrackingRectTag _lastToolTipTag;
NSString* __strong _toolTip;
}
@@ -67,37 +69,37 @@
// See above for rationale.
- (NSTrackingRectTag)addTrackingRect:(NSRect)rect
owner:(id)owner
- userData:(void *)data
+ userData:(void*)data
assumeInside:(BOOL)assumeInside {
- DCHECK(_trackingRectOwner == nil);
+ DCHECK_EQ(_trackingRectOwner, nil);
_trackingRectOwner = owner;
_trackingRectUserData = data;
return kTrackingRectTag;
}
-// Override of (apparently) a private NSView method(!) See above for rationale.
+// Override of a private NSView method. See above for rationale.
- (NSTrackingRectTag)_addTrackingRect:(NSRect)rect
owner:(id)owner
- userData:(void *)data
+ userData:(void*)data
assumeInside:(BOOL)assumeInside
useTrackingNum:(int)tag {
DCHECK(tag == 0 || tag == kTrackingRectTag);
- DCHECK(_trackingRectOwner == nil);
+ DCHECK_EQ(_trackingRectOwner, nil);
_trackingRectOwner = owner;
_trackingRectUserData = data;
return kTrackingRectTag;
}
-// Override of (apparently) a private NSView method(!) See above for rationale.
-- (void)_addTrackingRects:(NSRect *)rects
+// Override of a private NSView method. See above for rationale.
+- (void)_addTrackingRects:(NSRect*)rects
owner:(id)owner
- userDataList:(void **)userDataList
- assumeInsideList:(BOOL *)assumeInsideList
- trackingNums:(NSTrackingRectTag *)trackingNums
+ userDataList:(void**)userDataList
+ assumeInsideList:(BOOL*)assumeInsideList
+ trackingNums:(NSTrackingRectTag*)trackingNums
count:(int)count {
- DCHECK(count == 1);
+ DCHECK_EQ(count, 1);
DCHECK(trackingNums[0] == 0 || trackingNums[0] == kTrackingRectTag);
- DCHECK(_trackingRectOwner == nil);
+ DCHECK_EQ(_trackingRectOwner, nil);
_trackingRectOwner = owner;
_trackingRectUserData = userDataList[0];
trackingNums[0] = kTrackingRectTag;
@@ -106,11 +108,13 @@
// Override of a public NSView method, replacing the inherited functionality.
// See above for rationale.
- (void)removeTrackingRect:(NSTrackingRectTag)tag {
- if (tag == 0)
+ if (tag == 0) {
return;
+ }
if (tag == kTrackingRectTag) {
_trackingRectOwner = nil;
+ _trackingRectUserData = nullptr;
return;
}
@@ -121,19 +125,21 @@
}
// If any other tracking rect is being removed, we don't know how it was
- // created and it's possible there's a leak involved (see Radar 3500217).
+ // created and it's possible there's a leak involved (see rdar://3500217).
NOTREACHED();
}
-// Override of (apparently) a private NSView method(!)
-- (void)_removeTrackingRects:(NSTrackingRectTag *)tags count:(int)count {
+// Override of a private NSView method.
+- (void)_removeTrackingRects:(NSTrackingRectTag*)tags count:(int)count {
for (int i = 0; i < count; ++i) {
// SAFETY: count provided by caller.
NSTrackingRectTag tag = UNSAFE_BUFFERS(tags[i]);
- if (tag == 0)
+ if (tag == 0) {
continue;
- DCHECK(tag == kTrackingRectTag);
+ }
+ DCHECK_EQ(tag, kTrackingRectTag);
_trackingRectOwner = nil;
+ _trackingRectUserData = nullptr;
}
}
@@ -160,7 +166,7 @@
NSInteger windowNumber = self.window.windowNumber;
// Only send a fake mouse enter if the mouse is actually over the window,
- // versus over a window which overlaps it (see http://crbug.com/883269).
+ // versus over a window which overlaps it (see https://crbug.com/40092440).
if ([NSWindow windowNumberAtPoint:NSEvent.mouseLocation
belowWindowWithWindowNumber:0] != windowNumber) {
return;
@@ -207,7 +213,7 @@
// Sets the view's current tooltip, to be displayed at the current mouse
// location. (This does not make the tooltip appear -- as usual, it only
// appears after a delay.) Pass null to remove the tooltip.
-- (void)setToolTipAtMousePoint:(NSString *)string {
+- (void)setToolTipAtMousePoint:(NSString*)string {
NSString* toolTip = string.length == 0 ? nil : string;
if ((toolTip && _toolTip && [toolTip isEqualToString:_toolTip]) ||
(!toolTip && !_toolTip)) {
@@ -229,11 +235,11 @@
//
// By moving the call to -removeAllTooltips outside of the conditional,
// we can ensure any visible tooltip will be removed from the screen.
- // See crbug.com/1409942.
+ // See https://crbug.com/40889407.
//
// The strategy of removing all tooltips rather than the single one that
// was added comes from WebKit, like the rest of the code here. It
- // apparently works around some AppKit bug.
+ // apparently works around some AppKit bug, the same rdar://3500217 as above.
[self removeAllToolTips];
if (toolTip) {
NSRect wideOpenRect = NSMakeRect(-100000, -100000, 200000, 200000);
@@ -245,10 +251,10 @@
}
// NSView calls this to get the text when displaying the tooltip.
-- (NSString *)view:(NSView *)view
- stringForToolTip:(NSToolTipTag)tag
- point:(NSPoint)point
- userData:(void *)data {
+- (NSString*)view:(NSView*)view
+ stringForToolTip:(NSToolTipTag)tag
+ point:(NSPoint)point
+ userData:(void*)data {
return [_toolTip copy];
}
Original Bug Report
Potential Sandbox Escape via UAF in macOS ToolTipBaseView
Flapjack, 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 (UAF) in macOS tooltips can potentially be exploited by a compromised renderer. When a view is detached, tooltip cleanup fails to clear an internal pointer, which can later be dereferenced when the renderer sends a malicious tooltip update.
Affected files:
ui/base/cocoa/tool_tip_base_view.mm
Estimated timestamp from git blame: 2015-05-19
Description
A Use-After-Free (UAF) vulnerability exists in ui/base/cocoa/tool_tip_base_view.mm on macOS. ToolTipBaseView caches a raw pointer (_trackingRectUserData) to an internal AppKit NSToolTip struct. When a view is detached from its window and the tooltip is removed, the _trackingRectUserData pointer is not cleared, leaving it dangling. A compromised renderer can force the dereference of this dangling pointer by sending subsequent tooltip update IPC messages, potentially leading to a Sandbox Escape in the Browser process.
Technical Details
- Allocation: When a standard tooltip is displayed, AppKit allocates an
NSToolTipstruct and calls the overriddenaddTrackingRect:...method onToolTipBaseView(or a subclass likeRenderWidgetHostViewCocoa). Chromium caches a pointer to this struct in_trackingRectUserData. - Detachment: The view is detached from its
NSWindow(e.g., during tab tearing or window closure), soself.windowbecomesnil. - Removal: The tooltip is removed (either automatically or triggered by
setToolTipAtMousePoint:). AppKit calls the overriddenremoveTrackingRect:or_removeTrackingRects:count:. These methods correctly set_trackingRectOwnertonilbut fail to clear_trackingRectUserData. AppKit then frees theNSToolTipmemory, leaving_trackingRectUserDatadangling. - Malicious Update: A compromised renderer sends a
SetTooltipText(orUpdateTooltipUnderCursor) IPC message with a non-empty string. (Note: For some views, like Top Chrome WebUI, the bridge check only requires the app to be active, not the window to be valid). - Execution Path: The browser routes this to
-[ToolTipBaseView setToolTipAtMousePoint:].- It first calls
[self removeAllToolTips]. - It then attempts to add the new tooltip. Because
self.windowisnil, AppKit defers tracking rect creation and bypassesaddTrackingRect:, leaving the dangling_trackingRectUserDatauntouched. - It then calls
_sendToolTipMouseEntered. If the mouse is over the desktop, a safety check (windowNumberAtPoint) evaluates to 0, matchingself.window.windowNumber(which is also 0), and bypasses the check. Otherwise, the attacker can send a second IPC update, triggering_sendToolTipMouseExited, which completely lacks a window check.
- It first calls
- UAF: Both paths construct a fake
NSEventusing the dangling_trackingRectUserDataand dispatch it toNSToolTipManager. AppKit dereferences the freed memory. - Exploitation: Because
NSToolTipis an AppKit struct, it is allocated in a macOS system malloc zone. PartitionAlloc-Everywhere (PA-E) only intercepts the default zone, meaning this allocation is likely unprotected by BackupRefPtr (BRP). An attacker controlling the renderer can spray the browser heap to control the reallocated memory, leading to RCE in the Browser process.
Suggested Fix
Ensure _trackingRectUserData is explicitly cleared alongside _trackingRectOwner.
// ui/base/cocoa/tool_tip_base_view.mm
- (void)removeTrackingRect:(NSTrackingRectTag)tag {
...
if (tag == kTrackingRectTag) {
_trackingRectOwner = nil;
_trackingRectUserData = nullptr; // FIX
return;
}
...
}
- (void)_removeTrackingRects:(NSTrackingRectTag *)tags count:(int)count {
...
DCHECK(tag == kTrackingRectTag);
_trackingRectOwner = nil;
_trackingRectUserData = nullptr; // FIX
}
}
Additionally, add robust nil-checks for self.window and _trackingRectUserData in _sendToolTipMouseEntered and _sendToolTipMouseExited.
Evaluated with Chrome root at commit: c1eba8ce379f5d218a2948fafbb5dd72cfa30529
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.
Raised in root component due to access or custom field issues on 1974931