High watch ~ plausible CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Sub-componentUser Education
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
The mapped commit may not contain the whole fix.
Reviewer notes: Core teardown files are absent; shown hunks are Ash test/API refactors, so the UAF fix is unverifiable here. Advisory CVSS 8.8 implies a network vector, contradicting the claimed 'local' reachability.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker496456528
Fix commit62fb787c83e5 (chromium/src) +775/-749
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedBatuhan Eşref KOÇ
Reported2026-03-26 (per vendor advisory)
Fix landed2026-04-16 (public git)
Disclosed2026-04-28
Patch gap12 days — fix public in source before the advisory

Background

`HelpBubble`
the user-education framework object representing an in-product help/IPH bubble; HelpBubbleViews/HelpBubbleViewsAsh are its Views- and Ash-backed implementations.
`MakeCloseSynchronous()` / `AddOnClosingCallback()` / `AddOnClosedCallback()`
teardown APIs introduced by the hardening: they let a bubble be closed synchronously and split the single close callback into “closing” and “closed” phases so callers do not run code against a half-destroyed object.
`HelpBubbleFactoryRegistry`
the registry that owns/creates help bubbles; during teardown it previously could briefly hold a reference to a bubble that had already been deleted.
Reland
this commit re-applies an earlier hardening CL (e7b90968...) that had been reverted twice over ChromeOS component-build test failures; the functional security change is the hardening, re-added here with HelpBubbleViewsAsh kept for test-build safety.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Core teardown files are absent; shown hunks are Ash test/API refactors, so the UAF fix is unverifiable here. Advisory CVSS 8.8 implies a network vector, contradicting the claimed 'local' reachability.
Per the commit description this is a reland of “[User Education] Harden help bubble teardown,” which addresses use-after-free in Views-based help-bubble teardown: execution paths that invoke callbacks could delete this, and code paths reachable during teardown did not early-exit once teardown had begun, so they ran against a freed HelpBubble/bubble view. The original CL fixed this by making all callback-invoking paths safe against this being deleted, early-exiting teardown-reachable paths, enabling synchronous close via MakeCloseSynchronous(), splitting AddOnCloseCallback() into AddOnClosingCallback()/AddOnClosedCallback(), and ensuring HelpBubbleFactoryRegistry no longer retains a reference to a possibly-deleted bubble. The hunks actually shown in this diff are the Ash-side adjustments and tests: removal of UserEducationHelpBubbleController::GetHelpBubbleId (which cached a raw bubble_view() pointer and dereferenced it), replacement of bubble_view() with bubble_view_for_testing(), reinstatement of the HelpBubbleViewsAsh wrapper, and conversion of HelpBubble::Close() call sites to pass an explicit CloseReason (kProgrammaticallyClosed). The core teardown-hardening source files named in the message (help_bubble.h|cc, help_bubble_view.h|cc, help_bubble_views.h|cc) are not present in the shown hunks, so the detailed fix mechanism is attributed from the commit description rather than demonstrated line-by-line here.
Key insight
The core mistake is help-bubble teardown re-entering code paths and firing callbacks that could free the bubble while references to it were still in use; the fix hardens teardown so every callback path is this-deletion-safe, teardown-reachable paths early-exit, and Close() carries an explicit CloseReason.

Attack Path

  1. Show a help bubble Trigger an in-product-help/user-education flow that creates a Views-based HelpBubble (nearly all of them are Views-based, per the description).
  2. Initiate teardown Cause the bubble to close (programmatically, by anchor destruction, or widget teardown), entering the close/destroy path.
  3. Reentrant callback during destruction A close callback or teardown-reachable code path runs while the bubble is being destroyed; pre-fix it did not early-exit and could execute against the partially/fully freed object, or HelpBubbleFactoryRegistry briefly held a reference to the deleted bubble.
  4. Use-after-free The dangling reference/this is dereferenced after free (exploitation beyond the UAF is (inferred); this diff does not demonstrate a concrete primitive).

Impact Assessment

An attacker able to drive help-bubble lifecycle transitions (showing and tearing down IPH bubbles) in the browser-process UI can reach a use-after-free during teardown of Views/Ash help bubbles; the precondition is triggering a close/destroy sequence that fires callbacks or retains a stale reference. The vendor advisory rates this high severity with impact “Use after free in Views” and reports no in-the-wild exploitation; because this bubble machinery runs in the privileged browser process, any escalation beyond the UAF is (inferred) and not shown by the patch.

Changed Functions

FunctionChangeNotes
for
ash/user_education/user_education_help_bubble_controller.cc
modified
if
ash/user_education/user_education_help_bubble_controller.cc
modified
ElementContext
ash/user_education/user_education_help_bubble_controller.h
modified
ElementIdentifier
ash/user_education/user_education_help_bubble_controller.h
modified
HelpBubble
ash/user_education/user_education_help_bubble_controller.h
modified

Files Changed

  • ash/BUILD.gn
  • ash/user_education/user_education_help_bubble_controller.cc
  • ash/user_education/user_education_help_bubble_controller.h
  • ash/user_education/user_education_help_bubble_controller_unittest.cc

Audit Directions

  • Callback-driven teardown
    Audit widget/bubble/dialog destruction paths that invoke user-supplied or observer callbacks; ensure every such path is safe against this being deleted and that teardown-reachable methods early-exit once destruction starts.
  • Registry-held raw pointers
    Look for registries/controllers that cache raw pointers to UI objects (e.g. bubble_view()) across asynchronous close; these can dangle when the owning object is destroyed during teardown.
  • Two-phase close semantics
    Review APIs that collapse “closing” and “closed” into one callback or lack an explicit CloseReason; splitting them (as with AddOnClosingCallback()/AddOnClosedCallback()) prevents callers acting on a destroyed object.
From 62fb787c83e538d31a5aebe8cf402f340f5f06fd Mon Sep 17 00:00:00 2001
From: Dana Fried <dfried@chromium.org>
Date: Thu, 16 Apr 2026 11:28:22 -0700
Subject: [PATCH] Reland "[User Education] Harden help bubble teardown"

This reverts commit ba9f9ef4244dd3a351ee3d57a00ce4edb5821b8f.

Reason for revert: Fixed tests on linux-chromeos-debug CI builder.

Tests on that builder are one of the few component builds left in
Desktop chrome; the tests were using metadata that was not component
build safe. We added back in the HelpBubbleViewsAsh class to make those
tests safe on that builder. This should not appreciably affect
production Chrome.

Original change's description:
> Revert "[User Education] Harden help bubble teardown"
>
> This reverts commit e7b909682621f1b969f7f845fc9c41728a03072c.
>
> Reason for revert: Failing tests UserEducationHelpBubbleControllerTest.Metadata All/HelpBubbleFactoryViewsAshBrowserTest.CreateBubble/1
> Example: https://ci.chromium.org/ui/p/chromium/builders/ci/linux-chromeos-dbg/44923/overview
>
> Original change's description:
> > [User Education] Harden help bubble teardown
> >
> > This changes all Views-based help bubbles (which is nearly all of them)
> > to use MakeCloseSynchronous(). It also streamlines the teardown logic to
> > prevent UAFs:
> >  - All execution paths in help bubbles that can call callbacks are
> >    safe against `this` being deleted.
> >  - Code paths that could be called during teardown early-exit if
> >    teardown has started.
> >  - Help bubble teardown can be synchronous if desired.
> >  - `HelpBubbleFactoryRegistry` will no longer briefly hold a reference
> >    to a help bubble that may have been deleted.
> >  - HelpBubbleViewsAsh has been eliminated in favor of wrapping a
> >    HelpBubbleViewAsh in a HelpBubbleViews.
> >
> > This CL breaks `AddOnCloseCallback()` into `AddOnClosingCallback()` and
> > `AddOnClosedCallback()`, and uses them in the appropriate places.
> >
> > Primary files to review would be:
> >  - help_bubble.h|cc
> >  - help_bubble_view.h|cc
> >  - help_bubble_views.h|cc
> >
> > The vast majority of other changes are just refactors due to API
> > changes.
> >
> > In a follow-up, we may replace `AddOnCloseCallback()` with a
> > `MakeCloseSynchronous()` type method that further ties it to help bubble
> > ownership.
> >
> > Bug: 496456528
> > Change-Id: Ie999b965cf40e2e091f2de99f2f7b80338bcce15
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7744206
> > Commit-Queue: Dana Fried <dfried@chromium.org>
> > Reviewed-by: Elly <ellyjones@chromium.org>
> > Reviewed-by: Erik Chen <erikchen@chromium.org>
> > Cr-Commit-Position: refs/heads/main@{#1614772}
>
> Bug: 496456528
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Change-Id: I9e890e46cd13bfc1f58520eefffa11a1805e0da2
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7762386
> Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
> Owners-Override: Simon Ziegltrum <ziegltrum@google.com>
> Commit-Queue: Simon Ziegltrum <ziegltrum@google.com>
> Cr-Commit-Position: refs/heads/main@{#1615005}

Bug: 496456528
Change-Id: Ib602979553a5cd8d257c317834258625fb8ca763
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7765574
Reviewed-by: Darryl James <dljames@chromium.org>
Auto-Submit: Dana Fried <dfried@chromium.org>
Reviewed-by: Eshwar Stalin <estalin@chromium.org>
Commit-Queue: Dana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1616015}
---

diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 710a6b4..9453755 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -4704,6 +4704,7 @@
     "//components/ukm:test_support",
     "//components/user_education/common",
     "//components/user_education/common:events",
+    "//components/user_education/views",
     "//components/user_education/views:test_support",
     "//components/user_manager",
     "//components/user_manager:test_support",
@@ -5435,6 +5436,7 @@
     "//components/session_manager/core",
     "//components/session_manager/core:test_support",
     "//components/user_education/common",
+    "//components/user_education/views",
     "//components/user_manager",
     "//components/viz/test:test_support",
     "//device/bluetooth",
diff --git a/ash/user_education/user_education_help_bubble_controller.cc b/ash/user_education/user_education_help_bubble_controller.cc
index 9063861..49903048 100644
--- a/ash/user_education/user_education_help_bubble_controller.cc
+++ b/ash/user_education/user_education_help_bubble_controller.cc
@@ -67,30 +67,6 @@
   return g_instance;
 }
 
-std::optional<HelpBubbleId> UserEducationHelpBubbleController::GetHelpBubbleId(
-    ui::ElementIdentifier element_id,
-    ui::ElementContext element_context) const {
-  if (help_bubble_ && help_bubble_->IsA<HelpBubbleViewsAsh>()) {
-    // Cache the `bubble_view` with its associated anchor.
-    auto* bubble_view = help_bubble_->AsA<HelpBubbleViewsAsh>()->bubble_view();
-    auto* anchor_view = bubble_view->GetAnchorView();
-
-    // Find all `tracked_views` matching `element_id` and `element_context`.
-    const views::ElementTrackerViews::ViewList tracked_views =
-        views::ElementTrackerViews::GetInstance()->GetAllMatchingViews(
-            element_id, element_context);
-
-    // A help bubble exists for a `tracked_view` if the `tracked_view` is the
-    // `anchor_view` for the help bubble.
-    for (const auto* tracked_view : tracked_views) {
-      if (tracked_view == anchor_view) {
-        return bubble_view->id();
-      }
-    }
-  }
-  return std::nullopt;
-}
-
 base::CallbackListSubscription
 UserEducationHelpBubbleController::AddHelpBubbleAnchorBoundsChangedCallback(
     base::RepeatingClosure callback) {
diff --git a/ash/user_education/user_education_help_bubble_controller.h b/ash/user_education/user_education_help_bubble_controller.h
index ef8aaef8..8450137 100644
--- a/ash/user_education/user_education_help_bubble_controller.h
+++ b/ash/user_education/user_education_help_bubble_controller.h
@@ -16,11 +16,6 @@
 #include "base/memory/raw_ptr.h"
 #include "base/types/pass_key.h"
 
-namespace ui {
-class ElementContext;
-class ElementIdentifier;
-}  // namespace ui
-
 namespace user_education {
 class HelpBubble;
 }  // namespace user_education
@@ -46,15 +41,6 @@
   // NOTE: Exists if and only if user education features are enabled.
   static UserEducationHelpBubbleController* Get();
 
-  // Returns the unique identifier for the help bubble currently being shown for
-  // the tracked element associated with the specified `element_id` in the
-  // specified `element_context`. If no help bubble is currently being shown for
-  // the tracked element or if the tracked element does not exist, an absent
-  // value is returned.
-  std::optional<HelpBubbleId> GetHelpBubbleId(
-      ui::ElementIdentifier element_id,
-      ui::ElementContext element_context) const;
-
   // Adds a `callback` to be invoked whenever a help bubble's anchor bounds
   // change until the returned subscription is destroyed.
   [[nodiscard]] base::CallbackListSubscription
diff --git a/ash/user_education/user_education_help_bubble_controller_unittest.cc b/ash/user_education/user_education_help_bubble_controller_unittest.cc
index 202198c..87ae543 100644
--- a/ash/user_education/user_education_help_bubble_controller_unittest.cc
+++ b/ash/user_education/user_education_help_bubble_controller_unittest.cc
@@ -20,6 +20,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "components/user_education/common/help_bubble/help_bubble.h"
 #include "components/user_education/common/help_bubble/help_bubble_params.h"
+#include "components/user_education/views/help_bubble_views.h"
 #include "components/user_education/views/help_bubble_views_test_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/aura/window.h"
@@ -63,7 +64,9 @@
 
 HelpBubbleViewAsh* GetHelpBubbleView(HelpBubble* help_bubble) {
   return help_bubble->IsA<HelpBubbleViewsAsh>()
-             ? help_bubble->AsA<HelpBubbleViewsAsh>()->bubble_view()
+             ? views::AsViewClass<HelpBubbleViewAsh>(
+                   help_bubble->AsA<HelpBubbleViewsAsh>()
+                       ->bubble_view_for_testing())
              : nullptr;
 }
 
@@ -234,7 +237,8 @@
 
   // Destroy `help_bubble`.
   views::test::WidgetDestroyedWaiter waiter(help_bubble_view->GetWidget());
-  help_bubble->Close();
+  help_bubble->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ash/user_education/user_education_help_bubble_controller_unittest.cc b/ash/user_education/user_education_help_bubble_controller_unittest.cc
index 202198c..87ae543 100644
--- a/ash/user_education/user_education_help_bubble_controller_unittest.cc
+++ b/ash/user_education/user_education_help_bubble_controller_unittest.cc
@@ -20,6 +20,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "components/user_education/common/help_bubble/help_bubble.h"
 #include "components/user_education/common/help_bubble/help_bubble_params.h"
+#include "components/user_education/views/help_bubble_views.h"
 #include "components/user_education/views/help_bubble_views_test_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/aura/window.h"
@@ -63,7 +64,9 @@
 
 HelpBubbleViewAsh* GetHelpBubbleView(HelpBubble* help_bubble) {
   return help_bubble->IsA<HelpBubbleViewsAsh>()
-             ? help_bubble->AsA<HelpBubbleViewsAsh>()->bubble_view()
+             ? views::AsViewClass<HelpBubbleViewAsh>(
+                   help_bubble->AsA<HelpBubbleViewsAsh>()
+                       ->bubble_view_for_testing())
              : nullptr;
 }
 
@@ -234,7 +237,8 @@
 
   // Destroy `help_bubble`.
   views::test::WidgetDestroyedWaiter waiter(help_bubble_view->GetWidget());
-  help_bubble->Close();
+  help_bubble->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   waiter.Wait();
   help_bubble = nullptr;
   help_bubble_view = nullptr;
@@ -284,7 +288,8 @@
 
     // Close the `help_bubble`.
     ASSERT_TRUE(help_bubble);
-    help_bubble->Close();
+    help_bubble->Close(
+        user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
     help_bubble = nullptr;
 
     // Verify expectations.
diff --git a/ash/user_education/views/help_bubble_view_ash_unittest.cc b/ash/user_education/views/help_bubble_view_ash_unittest.cc
index de09c10a..17ae1d903 100644
--- a/ash/user_education/views/help_bubble_view_ash_unittest.cc
+++ b/ash/user_education/views/help_bubble_view_ash_unittest.cc
@@ -80,12 +80,12 @@
 
 // Verifies that help bubbles are contained within the correct parent window.
 TEST_F(HelpBubbleViewAshTest, ParentWindow) {
-  auto* const help_bubble_view = CreateHelpBubbleView();
-  EXPECT_TRUE(help_bubble_view->anchor_widget()
+  auto const help_bubble = CreateHelpBubbleView();
+  EXPECT_TRUE(help_bubble.bubble_view->anchor_widget()
                   ->GetNativeWindow()
                   ->GetRootWindow()
                   ->GetChildById(kShellWindowId_HelpBubbleContainer)
-                  ->Contains(help_bubble_view->GetWidget()->GetNativeWindow()));
+                  ->Contains(help_bubble.widget->GetNativeWindow()));
 }
 
 // HelpBubbleViewAshBodyIconTest -----------------------------------------------
@@ -154,8 +154,8 @@
   }
 
   // Create `help_bubble_view`.
-  auto* const help_bubble_view = CreateHelpBubbleView(std::move(params));
-  ASSERT_NE(help_bubble_view, nullptr);
+  auto const help_bubble = CreateHelpBubbleView(std::move(params));
+  ASSERT_NE(help_bubble.bubble_view, nullptr);
 
   // Cache `expected_body_icon` based on order of precedence.
   const gfx::VectorIcon& expected_body_icon =
@@ -167,7 +167,8 @@
       views::ElementTrackerViews::GetInstance()
           ->GetUniqueViewAs<views::ImageView>(
               HelpBubbleViewAsh::kBodyIconIdForTesting,
-              views::ElementTrackerViews::GetContextForView(help_bubble_view)),
+              views::ElementTrackerViews::GetContextForView(
+                  help_bubble.bubble_view)),
       Conditional(&expected_body_icon != &gfx::VectorIcon::EmptyIcon(),
                   Property(&views::ImageView::GetImageModel,
                            Eq(ui::ImageModel::FromVectorIcon(
@@ -178,33 +179,33 @@
 
 // Verifies that help bubbles have the appropriate background color.
 TEST_F(HelpBubbleViewAshTest, BackgroundColor) {
-  const auto* const help_bubble_view = CreateHelpBubbleView();
-  EXPECT_EQ(help_bubble_view->background_color(),
+  const auto help_bubble = CreateHelpBubbleView();
+  EXPECT_EQ(help_bubble.bubble_view->background_color(),
             cros_tokens::kCrosSysDialogContainer);
 }
 
 // Verifies that help bubbles can activate.
 TEST_F(HelpBubbleViewAshTest, CanActivate) {
-  const auto* const help_bubble_view = CreateHelpBubbleView();
-  EXPECT_TRUE(help_bubble_view->CanActivate());
+  const auto help_bubble = CreateHelpBubbleView();
+  EXPECT_TRUE(help_bubble.bubble_view->CanActivate());
 }
 
 TEST_F(HelpBubbleViewAshTest, RootViewAccessibleName) {
-  auto* const help_bubble_view = CreateHelpBubbleView();
+  auto const help_bubble = CreateHelpBubbleView();
   ui::AXNodeData root_view_data;
-  help_bubble_view->GetWidget()
-      ->GetRootView()
+  help_bubble.widget->GetRootView()
       ->GetViewAccessibility()
       .GetAccessibleNodeData(&root_view_data);
   EXPECT_EQ(
       root_view_data.GetString16Attribute(ax::mojom::StringAttribute::kName),
-      help_bubble_view->GetAccessibleWindowTitle());
+      help_bubble.bubble_view->GetAccessibleWindowTitle());
 }
 
 // Verifies that help bubbles do not handle events within their shadows.
 TEST_F(HelpBubbleViewAshTest, HitTest) {
-  auto* const help_bubble_view = CreateHelpBubbleView();
-  auto* const help_bubble_widget = help_bubble_view->GetWidget();
+  auto const help_bubble = CreateHelpBubbleView();
+  auto* const help_bubble_view = help_bubble.bubble_view.get();
+  auto* const help_bubble_widget = help_bubble.widget.get();
   auto* const help_bubble_window = help_bubble_widget->GetNativeWindow();
   auto* const root_window = help_bubble_window->GetRootWindow();
   auto* const root_window_targeter = root_window->targeter();
diff --git a/chrome/browser/ui/ash/user_education/views/help_bubble_factory_views_ash_browsertest.cc b/chrome/browser/ui/ash/user_education/views/help_bubble_factory_views_ash_browsertest.cc
index 87dc0b9..fe564005 100644
--- a/chrome/browser/ui/ash/user_education/views/help_bubble_factory_views_ash_browsertest.cc
+++ b/chrome/browser/ui/ash/user_education/views/help_bubble_factory_views_ash_browsertest.cc
@@ -33,11 +33,9 @@
 // Aliases.
 using ::ash::HelpBubbleContext;
 using ::ash::HelpBubbleViewAsh;
-using ::ash::HelpBubbleViewsAsh;
 using ::ash::kHelpBubbleContextKey;
 using ::user_education::HelpBubbleParams;
 using ::user_education::HelpBubbleView;
-using ::user_education::HelpBubbleViews;
 
 // Helpers ---------------------------------------------------------------------
 
@@ -111,6 +109,5 @@
 
   // The help `bubble` should be Ash-specific depending on `context`.
   bool is_ash_context = context == HelpBubbleContext::kAsh;
-  ASSERT_EQ(bubble->IsA<HelpBubbleViewsAsh>(), is_ash_context);
-  ASSERT_NE(bubble->IsA<HelpBubbleViews>(), is_ash_context);
+  ASSERT_EQ(bubble->IsA<ash::HelpBubbleViewsAsh>(), is_ash_context);
 }
diff --git a/chrome/browser/ui/user_education/show_promo_in_page_browsertest.cc b/chrome/browser/ui/user_education/show_promo_in_page_browsertest.cc
index baec88a1..00d4bd1e 100644
--- a/chrome/browser/ui/user_education/show_promo_in_page_browsertest.cc
+++ b/chrome/browser/ui/user_education/show_promo_in_page_browsertest.cc
@@ -98,7 +98,8 @@
   ASSERT_TRUE(handle->GetHelpBubbleForTesting()->is_open());
 
   // Closing the help bubble should destroy the object.
-  handle->GetHelpBubbleForTesting()->Close();
+  handle->GetHelpBubbleForTesting()->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   ASSERT_FALSE(handle);
 }
 
@@ -143,7 +144,8 @@
   ASSERT_TRUE(handle->GetHelpBubbleForTesting()->is_open());
 
   // Closing the help bubble should destroy the object.
-  handle->GetHelpBubbleForTesting()->Close();
+  handle->GetHelpBubbleForTesting()->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   ASSERT_FALSE(handle);
 }
 
@@ -180,7 +182,8 @@
   ASSERT_TRUE(handle->GetHelpBubbleForTesting()->is_open());
 
   // Closing the help bubble should destroy the object.
-  handle->GetHelpBubbleForTesting()->Close();
+  handle->GetHelpBubbleForTesting()->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   ASSERT_FALSE(handle);
 }
 
@@ -217,7 +220,8 @@
   ASSERT_TRUE(handle->GetHelpBubbleForTesting()->is_open());
 
   // Closing the help bubble should destroy the object.
-  handle->GetHelpBubbleForTesting()->Close();
+  handle->GetHelpBubbleForTesting()->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   ASSERT_FALSE(handle);
 }
 
@@ -361,6 +365,7 @@
   ASSERT_TRUE(handle->GetHelpBubbleForTesting()->is_open());
 
   // Closing the help bubble should destroy the object.
-  handle->GetHelpBubbleForTesting()->Close();
+  handle->GetHelpBubbleForTesting()->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
   ASSERT_FALSE(handle);
 }
diff --git a/chrome/browser/ui/views/user_education/custom_webui_help_bubble_interactive_uitest.cc b/chrome/browser/ui/views/user_education/custom_webui_help_bubble_interactive_uitest.cc
index 4624921..8443d2c 100644
--- a/chrome/browser/ui/views/user_education/custom_webui_help_bubble_interactive_uitest.cc
+++ b/chrome/browser/ui/views/user_education/custom_webui_help_bubble_interactive_uitest.cc
@@ -324,7 +324,10 @@
                               CustomWebUIHelpBubble::kWebViewIdForTesting),
       ClickElement(kWebViewElementId, kCancelButton),
       WaitForEvent(kToolbarAppMenuButtonElementId, kCallbackEvent),
-      Do([&help_bubble]() { help_bubble->Close(); }),
+      Do([&help_bubble]() {
+        help_bubble->Close(
+            user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
+      }),
       WaitForHide(CustomWebUIHelpBubble::kHelpBubbleIdForTesting),
       CheckIsAnchor(kToolbarAppMenuButtonElementId, false));
 }
@@ -431,7 +434,8 @@
       CheckIsDismissed(kCustomWebUIHelpBubbleTestFeature, true));
 }
 
-// Regression test for https://crbug.com/496456528
+// Regression tests for https://crbug.com/496456528
+
 IN_PROC_BROWSER_TEST_F(CustomWebUIHelpBubbleUiTest, ShowPromoAndCloseBrowser) {
   RunTestSequence(
       MaybeShowPromo(kCustomWebUIHelpBubbleTestFeature,
@@ -441,3 +445,24 @@
       WaitForHide(kBrowserViewElementId),
       WaitForHide(CustomWebUIHelpBubble::kHelpBubbleIdForTesting));
 }
+
+IN_PROC_BROWSER_TEST_F(CustomWebUIHelpBubbleUiTest, ShowPromoAndCloseBubble) {
+  RunTestSequence(
+      MaybeShowPromo(kCustomWebUIHelpBubbleTestFeature,
+                     CustomHelpBubbleShown{
+                         CustomWebUIHelpBubble::kHelpBubbleIdForTesting}),
+      WithView(CustomWebUIHelpBubble::kHelpBubbleIdForTesting,
+               [](views::View* view) { view->GetWidget()->Close(); }),
+      WaitForHide(CustomWebUIHelpBubble::kHelpBubbleIdForTesting));
+}
+
+IN_PROC_BROWSER_TEST_F(CustomWebUIHelpBubbleUiTest,
+                       ShowPromoAndCloseBubbleNow) {
+  RunTestSequence(
+      MaybeShowPromo(kCustomWebUIHelpBubbleTestFeature,
+                     CustomHelpBubbleShown{
+                         CustomWebUIHelpBubble::kHelpBubbleIdForTesting}),
+      WithView(CustomWebUIHelpBubble::kHelpBubbleIdForTesting,
+               [](views::View* view) { view->GetWidget()->CloseNow(); }),
+      WaitForHide(CustomWebUIHelpBubble::kHelpBubbleIdForTesting));
+}
diff --git a/chrome/browser/ui/views/user_education/help_bubble_factory_views_browsertest.cc b/chrome/browser/ui/views/user_education/help_bubble_factory_views_browsertest.cc
index ca0eaa8..d3bd7d8 100644
--- a/chrome/browser/ui/views/user_education/help_bubble_factory_views_browsertest.cc
+++ b/chrome/browser/ui/views/user_education/help_bubble_factory_views_browsertest.cc
@@ -77,7 +77,8 @@
   ASSERT_TRUE(help_bubble_);
   ASSERT_TRUE(help_bubble_->IsA<user_education::HelpBubbleViews>());
   EXPECT_TRUE(help_bubble_->is_open());
-  EXPECT_TRUE(help_bubble_->Close());
+  EXPECT_TRUE(help_bubble_->Close(
+      user_education::HelpBubble::CloseReason::kProgrammaticallyClosed));
   EXPECT_FALSE(help_bubble_->is_open());
 }
diff --git a/chrome/browser/ui/views/user_education/help_bubble_factory_webui_interactive_uitest.cc b/chrome/browser/ui/views/user_education/help_bubble_factory_webui_interactive_uitest.cc
index 4ccd7f5..5eabf44 100644
--- a/chrome/browser/ui/views/user_education/help_bubble_factory_webui_interactive_uitest.cc
+++ b/chrome/browser/ui/views/user_education/help_bubble_factory_webui_interactive_uitest.cc
@@ -155,7 +155,10 @@
   }
 
   auto CloseHelpBubble() {
-    return Do(base::BindLambdaForTesting([this]() { help_bubble_->Close(); }));
+    return Do(base::BindLambdaForTesting([this]() {
+      help_bubble_->Close(
+          user_education::HelpBubble::CloseReason::kProgrammaticallyClosed);
+    }));
   }
 
   auto CheckHandlerHasHelpBubble(ElementSpecifier anchor,
diff --git a/chrome/browser/ui/views/user_education/help_bubble_view_timeout_unittest.cc b/chrome/browser/ui/views/user_education/help_bubble_view_timeout_unittest.cc
index 0547162..7c62e19c 100644
--- a/chrome/browser/ui/views/user_education/help_bubble_view_timeout_unittest.cc
+++ b/chrome/browser/ui/views/user_education/help_bubble_view_timeout_unittest.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/ui/views/user_education/browser_user_education_service.h"
 #include "components/user_education/common/help_bubble/help_bubble_params.h"
 #include "components/user_education/views/help_bubble_view.h"
+#include "components/user_education/views/help_bubble_view_info.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/views/widget/widget_observer.h"
 
@@ -19,14 +20,7 @@
 using user_education::HelpBubbleButtonParams;
 using user_education::HelpBubbleParams;
... (truncated)
Loading diff…

Original Bug Report

reported by ba...@gmail.com

Security: T0 Heap-Use-After-Free in views::Widget during Browser Teardown

Steps to reproduce the problem

Hello, I encountered a crash while running tests on Chromium. I’m not sure if it’s related to my HTML file, but the terminal output indicates the root cause of the problem. Obviously, to recreate this problem, I’m compiling Chromium from scratch on a different computer. I know the problem will recur if I encounter this bubble when I log in, but I’ve only encountered it once in 4 days. I suspect it’s a promotional bubble shown to new users, so I’ll try compiling from scratch on another computer.

Problem Description

I downloaded and compiled Chromium for the first time on my computer; this was my first time using the Chromium browser. My HTML file contained instructions to open the calculator, and coincidentally, perhaps because I had just downloaded it, a blue promotional bubble for Chromium appeared in the upper right corner. At the same time, a window popped up asking if I wanted to open the calculator because of my HTML file, but I ignored both warnings and closed the tabs by clicking the ‘X’ buttons. Meanwhile, the browser crashed and gave an error output in my terminal. VULNERABILITY DETAILS Heap-Use-After-Free (Read of size 8) in the main Browser Process (T0), which completely bypasses the sandbox. The vulnerable object is views::Widget, and ASan confirms it is NOT protected by MiraclePtr/BackupRefPtr.

ROOT CAUSE ANALYSIS This is a synchronous race condition between Chromium’s Browser teardown sequence (Browser::~Browser()) and the macOS native window notification system (NSNotificationCenter).

Phase A (Allocation): The IPH system triggers a Help Bubble (e.g., CustomWebUIHelpBubble::CreateForController<extensions::ZeroStatePromoController>).

Phase B (The Free): When the browser is closed (via Cmd+Q), Browser::~Browser() initiates the destruction sequence. The BubbleWidget is destroyed and its memory is synchronously freed via user_education::HelpBubble::Close().

Phase C (The Use): Although the C++ object is freed, the macOS NSNotificationCenter fires a windowWillClose: notification. This invokes remote_cocoa::NativeWidgetNSWindowBridge::OnWindowWillClose(), which calls back into views::NativeWidgetMac::WindowDestroying() and views::Widget::HandleWidgetDestroying(). This attempts to read the freed C++ Widget state, causing a critical T0 UAF.

REPRODUCTION STEPS Note: Reproducing this consistently on a local, unbranded build is extremely difficult because the FeatureEngagementTracker heuristics block the ZeroStatePromo bubble from rendering reliably (e.g., it waits for “browser initialization complete” and checks Finch configs).

To reproduce internally:

  1. Use internal developer flags to force a WebUI Help Bubble to render (e.g., force IPH_ExtensionsZeroStatePromo on chrome://extensions).
  2. While that blue Help Bubble is actively visible on the macOS screen, force-close the browser (Cmd + Q).
  3. The ASan trace will immediately catch the UAF during teardown.

Please review the attached ASan log for the complete stack trace. Given the T0 context and lack of MiraclePtr protection, I kindly request a review of the severity.

Summary

Security: T0 Heap-Use-After-Free in views::Widget during Browser Teardown

Custom Questions

Type of crash:

browser

Crash state:

AddressSanitizer: heap-use-after-free (Read of size 8) Crash location: views::Widget::HandleWidgetDestroying()

Thread: T0 (Main Browser Process) MiraclePtr Status: NOT PROTECTED

  • Please see the attached ASan log (.txt) for the complete allocation, free, and crash stack traces.

Reporter credit:

Batuhan Eşref KOÇ

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker