Medium watch CVSS 8.3 chrome UAF 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in TopChrome
DescriptionUse after free in TopChrome
ComponentTopChrome
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker497487462
Fix commit53245ff272a9 (chromium/src) +14/-18
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-04-04 (public git)
Disclosed2026-05-05
Patch gap31 days — fix public in source before the advisory

Background

Toast / `ToastView`
a transient UI surface in the browser (TopChrome) that can display an optional override image (image_override) in its icon slot.
`ui::ImageModel`
a value type describing an image to render; the override was previously passed and stored by raw pointer rather than by value.
`raw_ptr<const ui::ImageModel>`
a non-owning (dangling-aware) pointer; it does not keep the pointee alive, so it dangles if the underlying ImageModel is destroyed while the pointer is still held.
`std::optional<ui::ImageModel>`
an owned-by-value optional; moving one into a member makes the view own the image for its own lifetime.

Root Cause Analysis

ToastController computed image_override as a raw const ui::ImageModel* pointing into params.image_override.value() and handed that pointer to the ToastView constructor, which stored it in a const raw_ptr<const ui::ImageModel> image_override_ member. The pointee’s lifetime (the ToastParams/image_override optional) was not guaranteed to outlive the ToastView, so by the time ToastView::OnThemeChanged dereferenced image_override_ (via IsCompatibleImageSize(image_override_) and icon_view_->SetImage(*image_override_)), the backing ImageModel could already have been freed — a use-after-free. The invariant that image_override_ points to live memory for the view’s full lifetime was not maintained.

The fix changes ownership: the constructor now takes std::optional<ui::ImageModel> image_override by value and stores it as const std::optional<ui::ImageModel> image_override_ via std::move, so the view owns a copy; ToastController simply passes params.image_override directly. IsCompatibleImageSize is changed to take const ui::ImageModel&, and OnThemeChanged now uses image_override_.has_value() and image_override_.value(), eliminating the dangling raw pointer.

Key insight
The core mistake was storing a non-owning raw_ptr to an ImageModel whose lifetime was not tied to the view; the fix stores the image by value in a std::optional<ui::ImageModel> so the view owns it and cannot dangle.

Attack Path

  1. Trigger a toast with an image override Cause a toast to be shown with params.image_override populated, so ToastView is constructed holding a pointer into that image.
  2. Outlive the backing image Let the ImageModel backing params.image_override be destroyed while the ToastView persists, leaving image_override_ dangling.
  3. Force a theme change / repaint A theme change invokes ToastView::OnThemeChanged, which dereferences the stale image_override_ in IsCompatibleImageSize and SetImage, triggering the use-after-free (inferred: the specific lifetime race and any exploitation primitive are not detailed in the diff).

Impact Assessment

The bug is a use-after-free of a ui::ImageModel referenced by a browser-process UI view (ToastView in TopChrome), reachable when a toast with an image override is rendered and later repainted after the backing image is freed; memory corruption beyond the dangling dereference is not demonstrated by the diff and any escalation is (inferred). The advisory rates it medium and classifies it as “Use after free in TopChrome”; no in-the-wild exploitation is stated.

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/toasts/toast_view_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/toasts/toast_controller.cc
  • chrome/browser/ui/toasts/toast_view.cc
  • chrome/browser/ui/toasts/toast_view.h
  • chrome/browser/ui/toasts/toast_view_browsertest.cc

Audit Directions

  • `raw_ptr` into caller-owned params
    Audit view/controller constructors that store raw_ptr/raw pointers into fields of a *Params struct or optional whose lifetime is not guaranteed to exceed the view’s.
  • Deferred dereference in paint/theme callbacks
    Look for OnThemeChanged/OnPaint-style callbacks that dereference stored pointers to data captured at construction time, where the data may have been freed in between.
  • Optional-to-pointer conversions
    Flag patterns that take &optional.value() to produce a raw pointer handed to a longer-lived object; prefer moving the std::optional/value into the owner.
From 53245ff272a9b4118ed8129092263b23e012fdc3 Mon Sep 17 00:00:00 2001
From: Alison Gale <[email protected]>
Date: Fri, 03 Apr 2026 17:47:23 -0700
Subject: [PATCH] Prevent UAF by storing ImageModel rather than raw_ptr


Bug: 497487462
Change-Id: Ieb551cfcda46428c1569c355b1645b5cfa69cc56
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7729341
Reviewed-by: Steven Luong <[email protected]>
Commit-Queue: Alison Gale <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610045}
---

diff --git a/chrome/browser/ui/toasts/toast_controller.cc b/chrome/browser/ui/toasts/toast_controller.cc
index e8ee971a..1b41d0c8 100644
--- a/chrome/browser/ui/toasts/toast_controller.cc
+++ b/chrome/browser/ui/toasts/toast_controller.cc
@@ -302,10 +302,6 @@
     return;
   }
 
-  const ui::ImageModel* image_override = params.image_override.has_value()
-                                             ? &params.image_override.value()
-                                             : nullptr;
-
   const std::u16string body_string =
       params.body_string_override.has_value()
           ? params.body_string_override.value()
@@ -313,7 +309,7 @@
                          params.body_string_replacement_params,
                          params.body_string_cardinality_param);
   auto toast_view = std::make_unique<toasts::ToastView>(
-      anchor_view, body_string, spec->icon(), image_override,
+      anchor_view, body_string, spec->icon(), params.image_override,
       ShouldRenderToastOverWebContents(),
       base::BindRepeating(&RecordToastDismissReason, params.toast_id));
 
diff --git a/chrome/browser/ui/toasts/toast_view.cc b/chrome/browser/ui/toasts/toast_view.cc
index c43d4907..3d5ca1c4 100644
--- a/chrome/browser/ui/toasts/toast_view.cc
+++ b/chrome/browser/ui/toasts/toast_view.cc
@@ -56,9 +56,9 @@
   return transform;
 }
 
-bool IsCompatibleImageSize(const ui::ImageModel* image) {
+bool IsCompatibleImageSize(const ui::ImageModel& image) {
   const auto intended_size = toasts::ToastView::GetIconSize();
-  const auto image_size = image->Size();
+  const auto image_size = image.Size();
   return image_size.width() == intended_size &&
          image_size.height() == intended_size;
 }
@@ -106,14 +106,14 @@
     views::View* anchor_view,
     const std::u16string& toast_text,
     const gfx::VectorIcon& icon,
-    const ui::ImageModel* image_override,
+    std::optional<ui::ImageModel> image_override,
     bool render_toast_over_web_contents,
     base::RepeatingCallback<void(ToastCloseReason)> toast_close_callback)
     : BubbleDialogDelegateView(anchor_view, views::BubbleBorder::NONE),
       AnimationDelegateViews(this),
       toast_text_(toast_text),
       icon_(icon),
-      image_override_(image_override),
+      image_override_(std::move(image_override)),
       render_toast_over_web_contents_(render_toast_over_web_contents),
       toast_close_callback_(std::move(toast_close_callback)) {
   SetBackgroundColor(ui::kColorToastBackgroundProminent);
@@ -424,8 +424,9 @@
 void ToastView::OnThemeChanged() {
   BubbleDialogDelegateView::OnThemeChanged();
   const auto* color_provider = GetColorProvider();
-  if (image_override_ != nullptr && IsCompatibleImageSize(image_override_)) {
-    icon_view_->SetImage(*image_override_);
+  if (image_override_.has_value() &&
+      IsCompatibleImageSize(image_override_.value())) {
+    icon_view_->SetImage(image_override_.value());
   } else {
     icon_view_->SetImage(ui::ImageModel::FromVectorIcon(
         *icon_, color_provider->GetColor(ui::kColorToastForeground),
diff --git a/chrome/browser/ui/toasts/toast_view.h b/chrome/browser/ui/toasts/toast_view.h
index 0f5074b6..3f11d0a 100644
--- a/chrome/browser/ui/toasts/toast_view.h
+++ b/chrome/browser/ui/toasts/toast_view.h
@@ -60,7 +60,7 @@
       views::View* anchor_view,
       const std::u16string& toast_text,
       const gfx::VectorIcon& icon,
-      const ui::ImageModel* image_override,
+      std::optional<ui::ImageModel> image_override,
       bool should_hide_ui_for_fullscreen,
       base::RepeatingCallback<void(ToastCloseReason)> on_toast_close_callback);
   ~ToastView() override;
@@ -124,7 +124,7 @@
 
   const std::u16string toast_text_;
   const raw_ref<const gfx::VectorIcon> icon_;
-  const raw_ptr<const ui::ImageModel> image_override_;
+  const std::optional<ui::ImageModel> image_override_;
   bool render_toast_over_web_contents_;
   bool has_close_button_ = false;
   bool has_action_button_ = false;
diff --git a/chrome/browser/ui/toasts/toast_view_browsertest.cc b/chrome/browser/ui/toasts/toast_view_browsertest.cc
index d290db84..ab106f90 100644
--- a/chrome/browser/ui/toasts/toast_view_browsertest.cc
+++ b/chrome/browser/ui/toasts/toast_view_browsertest.cc
@@ -52,16 +52,16 @@
     if (!options_.text.empty()) {
       toast_text = options_.text;
     }
+    std::optional<ui::ImageModel> image_override;
     if (options_.add_image_override) {
       int size = toasts::ToastView::GetIconSize();
-      image_override_ =
-          std::make_unique<ui::ImageModel>(ui::ImageModel::FromImage(
-              gfx::test::CreateImage(size, size, 0xff0000)));
+      image_override = ui::ImageModel::FromImage(
+          gfx::test::CreateImage(size, size, 0xff0000));
     }
     std::unique_ptr<toasts::ToastView> toast =
         std::make_unique<toasts::ToastView>(
             anchor_view_, toast_text, vector_icons::kLinkIcon,
-            image_override_.get(), false, base::DoNothing());
+            std::move(image_override), false, base::DoNothing());
     if (options_.add_close_button) {
       toast->AddCloseButton(base::DoNothing());
     }
@@ -99,7 +99,6 @@
 
  private:
   raw_ptr<views::View> anchor_view_;
-  std::unique_ptr<ui::ImageModel> image_override_;
   raw_ptr<toasts::ToastView> toast_;
   raw_ptr<views::Widget> widget_;
   ToastOptions options_;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/toasts/toast_view_browsertest.cc b/chrome/browser/ui/toasts/toast_view_browsertest.cc
index d290db84..ab106f90 100644
--- a/chrome/browser/ui/toasts/toast_view_browsertest.cc
+++ b/chrome/browser/ui/toasts/toast_view_browsertest.cc
@@ -52,16 +52,16 @@
     if (!options_.text.empty()) {
       toast_text = options_.text;
     }
+    std::optional<ui::ImageModel> image_override;
     if (options_.add_image_override) {
       int size = toasts::ToastView::GetIconSize();
-      image_override_ =
-          std::make_unique<ui::ImageModel>(ui::ImageModel::FromImage(
-              gfx::test::CreateImage(size, size, 0xff0000)));
+      image_override = ui::ImageModel::FromImage(
+          gfx::test::CreateImage(size, size, 0xff0000));
     }
     std::unique_ptr<toasts::ToastView> toast =
         std::make_unique<toasts::ToastView>(
             anchor_view_, toast_text, vector_icons::kLinkIcon,
-            image_override_.get(), false, base::DoNothing());
+            std::move(image_override), false, base::DoNothing());
     if (options_.add_close_button) {
       toast->AddCloseButton(base::DoNothing());
     }
@@ -99,7 +99,6 @@
 
  private:
   raw_ptr<views::View> anchor_view_;
-  std::unique_ptr<ui::ImageModel> image_override_;
   raw_ptr<toasts::ToastView> toast_;
   raw_ptr<views::Widget> widget_;
   ToastOptions options_;
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.