CVE-2025-1916
Overview
Background
- `ProfilePickerHandler`
- the WebUI message handler backing the Chrome profile picker page, owning the C++ side that responds to JavaScript-initiated actions like creating a new profile.
- `base::Unretained`
- a binding helper that captures a raw, non-owning pointer to an object, asserting the object will outlive the callback without any lifetime tracking.
- `base::WeakPtr`
- a self-invalidating pointer, handed out by a
base::WeakPtrFactory, that automatically becomes null once its owning object is destroyed so a bound callback is silently dropped instead of dereferencing freed memory. - `ProfilePicker::SwitchToSignedOutPostIdentityFlow`
- an asynchronous flow that finishes signed-out profile creation and later invokes a completion callback, potentially after the picker and its handler have been torn down.
Root Cause Analysis
When the user created a local profile from the picker, ProfilePickerHandler::HandleContinueWithoutAccount invoked ProfilePicker::SwitchToSignedOutPostIdentityFlow and passed a completion callback bound with base::Unretained(this) to ProfilePickerHandler::OnProfileCreationFinished. The code assumed the picker (and therefore the ProfilePickerHandler) would always still be alive when the asynchronous flow completed, but that invariant does not hold: the picker window can be closed or the handler destroyed while the post-identity flow is still pending. Because base::Unretained performs no lifetime validation, the deferred callback would dereference and call into a freed ProfilePickerHandler, a classic use-after-free.
The fix replaces base::Unretained(this) with weak_factory_.GetWeakPtr(), so if the handler has been destroyed the callback is simply not run instead of touching freed memory.
base::Unretained(this) under a false assumption that the handler always outlives the flow; switching to a base::WeakPtr from weak_factory_ makes the callback lifetime-safe by automatically canceling it when the handler is gone.Attack Path
- Start profile creation
The victim opens the profile picker and begins creating a new local (signed-out) profile, triggering
ProfilePickerHandler::HandleContinueWithoutAccount. - Launch async flow
The handler calls
ProfilePicker::SwitchToSignedOutPostIdentityFlowwith a callback bound viabase::Unretained(this), schedulingOnProfileCreationFinishedfor later. - Destroy the handler
Before the flow completes, the picker is closed or navigated away, destroying the
ProfilePickerHandlerwhile its rawthispointer remains captured in the pending callback. - Trigger the stale callback
The post-identity flow completes and invokes the callback, dereferencing the freed
ProfilePickerHandlerand callingOnProfileCreationFinishedon reclaimed memory. - Exploit the freed object With controlled heap grooming to reoccupy the freed allocation, the attacker turns the dangling call into corruption or control-flow hijack within the browser process.
Impact Assessment
ProfilePickerHandler in the privileged browser process. Exploited with heap manipulation, this can yield memory corruption and potentially arbitrary code execution outside the renderer sandbox. The precondition is that the asynchronous SwitchToSignedOutPostIdentityFlow completion outlives the handler, which is reachable through normal picker close/teardown timing.Files Changed
chrome/browser/ui/webui/signin/profile_picker_handler.cc
Audit Directions
- Unretained across async boundariesSearch for
base::Unretained(this)passed into callbacks handed to asynchronous or deferred flows, and verify the bound object is guaranteed to outlive every invocation rather than assumed to. - WebUI handler lifetimesReview WebUI message handlers that schedule work outliving the page (like the profile picker, signin, and settings handlers), since the page and handler can be torn down while callbacks are still pending.
- Comment-justified lifetime assumptionsTreat code comments that assert “this will always be alive” as red flags to validate, since they often paper over real teardown races that a
base::WeakPtrwould handle safely.
Patch
From a7214df6d6c2bed2db9260f4f3bdac93e3e46342 Mon Sep 17 00:00:00 2001
From: Ryan Sultanem <rsult@google.com>
Date: Mon, 03 Feb 2025 06:58:07 -0800
Subject: [PATCH] [ProfilePicker] Fix potential UAF in the handler
Details of the analysis and reason for the fix in the linked bug.
Fixed: 376493203
Change-Id: Id42f0af3f4e6a4318ed97c617c38a4ba75bea397
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6225319
Reviewed-by: David Roger <droger@chromium.org>
Commit-Queue: Ryan Sultanem <rsult@google.com>
Cr-Commit-Position: refs/heads/main@{#1414896}
---
diff --git a/chrome/browser/ui/webui/signin/profile_picker_handler.cc b/chrome/browser/ui/webui/signin/profile_picker_handler.cc
index 30a71d4b..5beb496 100644
--- a/chrome/browser/ui/webui/signin/profile_picker_handler.cc
+++ b/chrome/browser/ui/webui/signin/profile_picker_handler.cc
@@ -514,12 +514,10 @@
ProfileMetrics::ADD_NEW_PROFILE_PICKER_LOCAL);
ProfilePicker::SwitchToSignedOutPostIdentityFlow(
profile_color,
- base::BindOnce(
- &ProfilePickerHandler::OnProfileCreationFinished,
- // `OnProfileCreationFinished` is called when we want to close the
- // profile picker. `ProfilePickerHandler` will always be initialized
- // when we get to that call because the picker will still be open.
- base::Unretained(this)));
+ base::BindOnce(&ProfilePickerHandler::OnProfileCreationFinished,
+ // `OnProfileCreationFinished` is called when we want to
+ // close the profile picker.
+ weak_factory_.GetWeakPtr()));
}
void ProfilePickerHandler::HandleGetSwitchProfile(
Original Bug Report
ProfilePickerHandler UAF via UI
Summary
Use after free in Profiles in Google Chrome allows a remote attacker who convinced a user to install a malicious extension to potentially exploit heap corruption via a crafted HTML page.
Reproduction Case
- For ease of reproduction, Apply
patch.diff. - Press the UI button to open the
Chromium Profile Managementwindow. - Open a new tab, move to
chrome://profile-picker, typechrome.send("continueWithoutAccount", [0]);into theDevTools’s console, and close the tab.
Root Cause Analysis
chrome/browser/ui/webui/signin/profile_picker_handler.cc
void ProfilePickerHandler::HandleContinueWithoutAccount(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
// profileColor is undefined for the default theme.
std::optional<SkColor> profile_color;
if (args[0].is_int())
profile_color = args[0].GetInt();
RecordProfilePickerAction(ProfilePickerAction::kLaunchNewProfile);
ProfileMetrics::LogProfileAddNewUser(
ProfileMetrics::ADD_NEW_PROFILE_PICKER_LOCAL);
ProfilePicker::SwitchToSignedOutPostIdentityFlow(
profile_color, profile_picked_time_on_startup_,
base::BindOnce(
&ProfilePickerHandler::OnProfileCreationFinished,
// `OnProfileCreationFinished` is called when we want to close the
// profile picker. `ProfilePickerHandler` will always be initialized
// when we get to that call because the picker will still be open.
base::Unretained(this))); // [0]
}
[0],HandleContinueWithoutAccountfunction transfers theProfilePickerHandler::OnProfileCreationFinishedfunction bound by thethispointer to theProfilePicker::SwitchToSignedOutPostIdentityFlowfunction as a callback param.
chrome/browser/ui/views/profiles/profile_picker_view.cc
void ProfilePicker::SwitchToSignedOutPostIdentityFlow(
std::optional<SkColor> profile_color,
base::TimeTicks profile_picked_time_on_startup,
base::OnceCallback<void(bool)> switch_finished_callback) {
if (g_profile_picker_view) {
g_profile_picker_view->SwitchToSignedOutPostIdentityFlow(
profile_color, profile_picked_time_on_startup,
std::move(switch_finished_callback)); // [1]
}
}
-
[1], In order to execute theg_profile_picker_view->SwitchToSignedOutPostIdentityFlowfunctions, the value of theg_profile_picker_viewpointer must be set in a normal flow. -
It is possible to create a complete chain that does not require user gestures by utilizing a chrome extension, but for simple proof, it is replaced by pressing a button directly to open the
Chromium Profile Managementwindow.
chrome/browser/profiles/profile_manager.cc
void ProfileManager::CreateMultiProfileAsync(
const std::u16string& name,
size_t icon_index,
bool is_hidden,
base::OnceCallback<void(Profile*)> initialized_callback,
base::OnceCallback<void(Profile*)> created_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!name.empty());
DCHECK(profiles::IsDefaultAvatarIconIndex(icon_index));
ProfileManager* profile_manager = g_browser_process->profile_manager();
ProfileAttributesStorage& storage =
profile_manager->GetProfileAttributesStorage();
base::FilePath new_path;
ProfileAttributesEntry* entry = nullptr;
do {
new_path = profile_manager->GenerateNextProfileDirectoryPath();
// The generated path should be unused and free to use.
DCHECK_EQ(profile_manager->GetProfileByPath(new_path), nullptr);
DCHECK(profile_manager->CanCreateProfileAtPath(new_path));
entry = storage.GetProfileAttributesWithPath(new_path);
} while (entry != nullptr);
ProfileAttributesInitParams init_params;
init_params.profile_path = new_path;
init_params.profile_name = name;
init_params.icon_index = icon_index;
init_params.is_ephemeral = is_hidden;
init_params.is_omitted = is_hidden;
storage.AddProfile(std::move(init_params));
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&NukeProfileFromDisk, new_path,
base::BindOnce(&ProfileManager::CreateProfileAsync,
profile_manager->weak_factory_.GetWeakPtr(),
new_path, std::move(initialized_callback),
std::move(created_callback)))); // [2]
}
[2], The callback function delivered in[0]was executed within theinitialized_callbackfunction, and it was posted to the thread pool to asynchronously process tasks.
content/browser/webui/web_ui_message_handler.cc
bool WebUIMessageHandler::IsJavascriptAllowed() {
return javascript_allowed_ && web_ui() && web_ui()->CanCallJavascript(); // [3]
}
[3], theProfilePickerHandler::OnProfileCreationFinishedfunction posted in the thread pool is called back, and UAF occurs by referring to thejavascript_allowed_variable ofWebUIMessageHandlerthat has already been freed within the function.
Bisect
https://source.chromium.org/chromium/chromium/src/+/4dd469427e8a336a1ca52f9a523b78785bbdb897
- In the process of reorganizing the function
ProfilePickerHandler::HandleCreateProfileAndOpenCustomizationDialogtoHandleContinueWithoutAccount, consideration of the lifetime of the object was omitted.
Recommended Patch
chrome/browser/ui/webui/signin/profile_picker_handler.h
class ProfilePickerHandler : public content::WebUIMessageHandler,
public content::WebContentsObserver,
#if BUILDFLAG(IS_CHROMEOS_LACROS)
public AccountProfileMapper::Observer,
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
public ProfileAttributesStorage::Observer {
...
base::WeakPtrFactory<ProfilePickerHandler> weak_factory_{this};
};
weak_factory_has already been declared within theProfilePickerHandlerclass.
fix.diff
--- a/chrome/browser/ui/webui/signin/profile_picker_handler.cc
+++ b/chrome/browser/ui/webui/signin/profile_picker_handler.cc
@@ -617,7 +617,7 @@
// `OnProfileCreationFinished` is called when we want to close the
// profile picker. `ProfilePickerHandler` will always be initialized
// when we get to that call because the picker will still be open.
- base::Unretained(this)));
+ weak_factory_.GetWeakPtr()));
}
void ProfilePickerHandler::HandleGetSwitchProfile(
- UAF can be prevented by modifying the existing
base::Unretained(this)toweak_factory_.GetWeakPtr()to verify that the object of the callback function is valid.
It is dangerous because an attacker can use Extension to create PoCs that do not require separate user gestures.
Version
131.0.6776.0
Credit
parkminchan, working for SSD Labs Korea.