CVE-2026-7970
Overview
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
ImageModelis 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.
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
- Trigger a toast with an image override
Cause a toast to be shown with
params.image_overridepopulated, soToastViewis constructed holding a pointer into that image. - Outlive the backing image
Let the
ImageModelbackingparams.image_overridebe destroyed while theToastViewpersists, leavingimage_override_dangling. - Force a theme change / repaint
A theme change invokes
ToastView::OnThemeChanged, which dereferences the staleimage_override_inIsCompatibleImageSizeandSetImage, triggering the use-after-free (inferred: the specific lifetime race and any exploitation primitive are not detailed in the diff).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/toasts/toast_view_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/toasts/toast_controller.ccchrome/browser/ui/toasts/toast_view.ccchrome/browser/ui/toasts/toast_view.hchrome/browser/ui/toasts/toast_view_browsertest.cc
Audit Directions
- `raw_ptr` into caller-owned paramsAudit view/controller constructors that store
raw_ptr/raw pointers into fields of a*Paramsstruct or optional whose lifetime is not guaranteed to exceed the view’s. - Deferred dereference in paint/theme callbacksLook 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 conversionsFlag patterns that take
&optional.value()to produce a raw pointer handed to a longer-lived object; prefer moving thestd::optional/value into the owner.
Patch
From 53245ff272a9b4118ed8129092263b23e012fdc3 Mon Sep 17 00:00:00 2001
From: Alison Gale <agale@chromium.org>
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 <stluong@chromium.org>
Commit-Queue: Alison Gale <agale@chromium.org>
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()
- ? ¶ms.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_;
Regression Test / PoC
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_;