Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Bug ClassUAF
Tracker503790201
Fix commitc24bb170bc65 (chromium/src) +33/-27
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
ui/base/cocoa/tool_tip_base_view.mm
modified
for
ui/base/cocoa/tool_tip_base_view.mm
modified

Files Changed

  • ui/base/cocoa/tool_tip_base_view.mm
From c24bb170bc654b5db62de638e93a37dc38a900c0 Mon Sep 17 00:00:00 2001
From: Avi Drissman <[email protected]>
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 <[email protected]>
Auto-Submit: Avi Drissman <[email protected]>
Commit-Queue: Bryan Oltman <[email protected]>
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];
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. Allocation: When a standard tooltip is displayed, AppKit allocates an NSToolTip struct and calls the overridden addTrackingRect:... method on ToolTipBaseView (or a subclass like RenderWidgetHostViewCocoa). Chromium caches a pointer to this struct in _trackingRectUserData.
  2. Detachment: The view is detached from its NSWindow (e.g., during tab tearing or window closure), so self.window becomes nil.
  3. Removal: The tooltip is removed (either automatically or triggered by setToolTipAtMousePoint:). AppKit calls the overridden removeTrackingRect: or _removeTrackingRects:count:. These methods correctly set _trackingRectOwner to nil but fail to clear _trackingRectUserData. AppKit then frees the NSToolTip memory, leaving _trackingRectUserData dangling.
  4. Malicious Update: A compromised renderer sends a SetTooltipText (or UpdateTooltipUnderCursor) 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).
  5. Execution Path: The browser routes this to -[ToolTipBaseView setToolTipAtMousePoint:].
    • It first calls [self removeAllToolTips].
    • It then attempts to add the new tooltip. Because self.window is nil, AppKit defers tracking rect creation and bypasses addTrackingRect:, leaving the dangling _trackingRectUserData untouched.
    • It then calls _sendToolTipMouseEntered. If the mouse is over the desktop, a safety check (windowNumberAtPoint) evaluates to 0, matching self.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.
  6. UAF: Both paths construct a fake NSEvent using the dangling _trackingRectUserData and dispatch it to NSToolTipManager. AppKit dereferences the freed memory.
  7. Exploitation: Because NSToolTip is 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

View on issue tracker