Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Profiles
DescriptionUse after free in Profiles
ComponentProfiles
Sub-componentProfilePicker
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.
Tracker376493203
Fix commita7214df6d6c2 (chromium/src) +4/-6
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
Creditedparkminchan, SSD Labs Korea
Reported2024-10-31 · bounty $3000 (per vendor advisory)
Fix landed2025-02-03 (public git)
Disclosed2025-03-04
Patch gap29 days — fix public in source before the advisory

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.

Key insight
The single mistake was binding an asynchronous completion callback to the handler with 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

  1. Start profile creation The victim opens the profile picker and begins creating a new local (signed-out) profile, triggering ProfilePickerHandler::HandleContinueWithoutAccount.
  2. Launch async flow The handler calls ProfilePicker::SwitchToSignedOutPostIdentityFlow with a callback bound via base::Unretained(this), scheduling OnProfileCreationFinished for later.
  3. Destroy the handler Before the flow completes, the picker is closed or navigated away, destroying the ProfilePickerHandler while its raw this pointer remains captured in the pending callback.
  4. Trigger the stale callback The post-identity flow completes and invokes the callback, dereferencing the freed ProfilePickerHandler and calling OnProfileCreationFinished on reclaimed memory.
  5. 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

An attacker who can drive the profile-picker flow (for example via a malicious or social-engineered interaction that closes the picker mid-creation) gains a use-after-free on a freed 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 boundaries
    Search 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 lifetimes
    Review 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 assumptions
    Treat 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::WeakPtr would handle safely.
From a7214df6d6c2bed2db9260f4f3bdac93e3e46342 Mon Sep 17 00:00:00 2001
From: Ryan Sultanem <[email protected]>
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 <[email protected]>
Commit-Queue: Ryan Sultanem <[email protected]>
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(
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. For ease of reproduction, Apply patch.diff.
  2. Press the UI button to open the Chromium Profile Management window.
  3. Open a new tab, move to chrome://profile-picker, type chrome.send("continueWithoutAccount", [0]); into the DevTools’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],HandleContinueWithoutAccount function transfers the ProfilePickerHandler::OnProfileCreationFinished function bound by the this pointer to the ProfilePicker::SwitchToSignedOutPostIdentityFlow function 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 the g_profile_picker_view->SwitchToSignedOutPostIdentityFlow functions, the value of the g_profile_picker_view pointer 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 Management window.

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 the initialized_callback function, 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], the ProfilePickerHandler::OnProfileCreationFinished function posted in the thread pool is called back, and UAF occurs by referring to the javascript_allowed_ variable of WebUIMessageHandler that has already been freed within the function.

Bisect

https://source.chromium.org/chromium/chromium/src/+/4dd469427e8a336a1ca52f9a523b78785bbdb897

  • In the process of reorganizing the function ProfilePickerHandler::HandleCreateProfileAndOpenCustomizationDialog to HandleContinueWithoutAccount, consideration of the lifetime of the object was omitted.

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 the ProfilePickerHandler class.

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) to weak_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.

View on issue tracker