CVE-2026-19166
Overview
Background
- `AuthenticatorRequestDialogController`
- the browser-process object that drives Chrome’s WebAuthn request UI. It is owned through the
ChromeAuthenticatorRequestDelegateof the tab that issued thenavigator.credentialscall. - `SetCurrentStep`
- moves the dialog model to a new
Stepand synchronously notifiesAuthenticatorRequestDialogModel::Observers viaOnStepTransition. Those observers can run arbitrary teardown, including destroying theWebContentsand, with it, the controller. - `base::WeakPtr` liveness check
- the pattern of taking
weak_factory_.GetWeakPtr()before a call that may deletethis, then testing it afterwards to detect that the object was destroyed.
Root Cause Analysis
Both AuthenticatorRequestDialogController::HideDialogAndDispatchToPlatformAuthenticator and AuthenticatorRequestDialogController::StartPasskeyUpgradeRequest call SetCurrentStep(...) and then keep using members of this. The first goes on to read ephemeral_state_.saved_authenticators_. The second reads passkey_upgrade_request_controller_. SetCurrentStep fires observers synchronously, and an observer that reacts to the new step by tearing down the tab (the regression test deletes the WebContents when the step becomes kPlatformAuthenticator) destroys the controller while it is still on the stack. The code assumed this outlives SetCurrentStep, and nothing checked that assumption. The subsequent member accesses then touch freed browser-process memory.
The fix takes a base::WeakPtr before each SetCurrentStep call and returns immediately if it has been invalidated, so no member is accessed after destruction.
this survives a call that synchronously notifies observers, and an observer is allowed to delete the controller. Adding a WeakPtr check right after SetCurrentStep stops the function from continuing on a destroyed object.Attack Path
- Start a WebAuthn request
A page calls
navigator.credentials.create/get, which creates aChromeAuthenticatorRequestDelegateand itsAuthenticatorRequestDialogControllerin the browser process. The test drives this from an ordinary HTTPS page. - Reach the vulnerable transition
The flow calls
HideDialogAndDispatchToPlatformAuthenticator(dispatching to the platform authenticator) orStartPasskeyUpgradeRequest(passkey-upgrade flow), and each one callsSetCurrentStep. - Teardown during notification
An
OnStepTransitionobserver destroys the owningWebContents/delegate, which frees the controller. The test simulates this directly. Which production observer or tab-close sequence does this in a real build is not shown in the patch (inferred). - Use after free
Before the patch, control returned into the freed controller, which then read
ephemeral_state_.saved_authenticators_orpasskey_upgrade_request_controller_. That is a use-after-free in the browser process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/webauthn/authenticator_request_dialog_controller.cc |
modified | |
WebAuthnUAFReproductionTestchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified | |
ifchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified |
Files Changed
chrome/browser/webauthn/authenticator_request_dialog_controller.ccchrome/browser/webauthn/chrome_webauthn_browsertest.cc
Audit Directions
- Other `SetCurrentStep` callersCheck every method in
AuthenticatorRequestDialogController(and inAuthenticatorRequestDialogModelusers) that callsSetCurrentStepor otherwise notifies observers and then reads members. Each one needs the sameWeakPtrguard or must not touch members afterwards. - Synchronous observer notification in browser UI controllersLook for
for (auto& observer : observers_)loops orOnStepTransition-style callbacks in WebAuthn, payments, and permission prompt controllers where an observer can close the tab or dialog. Then look at the code that runs after the notification in the caller. - Owner-chain teardownLook for objects owned indirectly by
WebContents(delegates and their controllers) whose methods can triggerWebContentsdestruction. Audit those methods for continued use ofthisafter any call that can close tabs, navigate, or hide dialogs.
Patch
From 25e013dd59d3e6eb9cafb4d730f86c532fd2789b Mon Sep 17 00:00:00 2001
From: Adem Derinel <derinel@google.com>
Date: Thu, 23 Jul 2026 01:03:39 -0700
Subject: [PATCH] Fix UAF in AuthenticatorRequestDialogController
Calling SetCurrentStep in HideDialogAndDispatchToPlatformAuthenticator
can trigger observers that destroy the dialog controller. This CL
introduces a WeakPtr check after SetCurrentStep to ensure the controller
is still alive before accessing its member variables. A regression test
has also been added to verify this behavior.
Fixed: 536584251
Change-Id: Idc80a34ec731967a6e4290f67c174cfaebfcddc3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8127931
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Commit-Queue: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1666890}
---
diff --git a/chrome/browser/webauthn/authenticator_request_dialog_controller.cc b/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
index b3e44f3..d1f9426 100644
--- a/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
+++ b/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
@@ -1000,7 +1000,12 @@
void AuthenticatorRequestDialogController::
HideDialogAndDispatchToPlatformAuthenticator(
std::optional<AuthenticatorType> type) {
+ base::WeakPtr<AuthenticatorRequestDialogController> weak_this =
+ weak_factory_.GetWeakPtr();
SetCurrentStep(Step::kPlatformAuthenticator);
+ if (!weak_this) {
+ return;
+ }
std::vector<AuthenticatorReference>& authenticators =
ephemeral_state_.saved_authenticators_;
@@ -2349,7 +2354,12 @@
}
void AuthenticatorRequestDialogController::StartPasskeyUpgradeRequest() {
+ base::WeakPtr<AuthenticatorRequestDialogController> weak_this =
+ weak_factory_.GetWeakPtr();
SetCurrentStep(Step::kPasskeyUpgrade);
+ if (!weak_this) {
+ return;
+ }
if (!passkey_upgrade_request_controller_) {
RecordPasskeyUpgradeResultHistogram(PasskeyUpgradeResult::kGpmDisabled);
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index e87a8c0..2ded26a 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -20,6 +20,7 @@
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
@@ -1860,7 +1861,11 @@
public ChromeAuthenticatorRequestDelegate::TestObserver,
public AuthenticatorRequestDialogModel::Observer {
public:
- WebAuthnUAFReproductionTest() = default;
+ WebAuthnUAFReproductionTest() {
+#if BUILDFLAG(IS_WIN)
+ win_api_.set_available(false);
+#endif
+ }
~WebAuthnUAFReproductionTest() override = default;
// ChromeAuthenticatorRequestDelegate::TestObserver:
@@ -1871,9 +1876,7 @@
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
- if (run_loop_) {
- run_loop_->Quit();
- }
+ delegate_shown_future_.SetValue(delegate);
}
void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
@@ -1883,7 +1886,9 @@
// AuthenticatorRequestDialogModel::Observer:
void OnStepTransition() override {
if (model_ &&
- model_->step() == AuthenticatorRequestDialogModel::Step::kClosed) {
+ (model_->step() == AuthenticatorRequestDialogModel::Step::kClosed ||
+ model_->step() ==
+ AuthenticatorRequestDialogModel::Step::kPlatformAuthenticator)) {
DeleteWebContents();
}
}
@@ -1919,8 +1924,13 @@
raw_ptr<ChromeAuthenticatorRequestDelegate> delegate_ = nullptr;
raw_ptr<AuthenticatorRequestDialogModel> model_ = nullptr;
- std::unique_ptr<base::RunLoop> run_loop_;
+ base::test::TestFuture<ChromeAuthenticatorRequestDelegate*>
+ delegate_shown_future_;
bool web_contents_deleted_ = false;
+#if BUILDFLAG(IS_WIN)
+ device::FakeWinWebAuthnApi win_api_;
+ device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override_{&win_api_};
+#endif
};
IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, CancelUAF) {
@@ -1947,10 +1957,7 @@
});
)");
- if (!delegate_) {
- run_loop_ = std::make_unique<base::RunLoop>();
- run_loop_->Run();
- }
+ ASSERT_TRUE(delegate_shown_future_.Wait());
ASSERT_TRUE(delegate_);
ASSERT_TRUE(delegate_->dialog_controller());
@@ -1962,4 +1969,37 @@
delegate_->dialog_controller()->CancelAuthenticatorRequest();
}
+IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, HideDialogUAF) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), https_server_.GetURL("www.example.com", "/title1.html")));
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Trigger WebAuthn flow asynchronously
+ content::ExecuteScriptAsync(web_contents, R"(
+ navigator.credentials.create({
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ rp: { name: "Example" },
+ user: {
+ id: new Uint8Array([1, 2, 3, 4]),
+ name: "test",
+ displayName: "test"
+ },
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+ timeout: 60000
+ }
+ });
+ )");
+
+ ASSERT_TRUE(delegate_shown_future_.Wait());
+ ASSERT_TRUE(delegate_);
+ ASSERT_TRUE(delegate_->dialog_controller());
+
+ // Trigger UAF
+ delegate_->dialog_controller()->HideDialogAndDispatchToPlatformAuthenticator(
+ std::nullopt);
+}
+
} // namespace
Regression Test / PoC
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index e87a8c0..2ded26a 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -20,6 +20,7 @@
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
@@ -1860,7 +1861,11 @@
public ChromeAuthenticatorRequestDelegate::TestObserver,
public AuthenticatorRequestDialogModel::Observer {
public:
- WebAuthnUAFReproductionTest() = default;
+ WebAuthnUAFReproductionTest() {
+#if BUILDFLAG(IS_WIN)
+ win_api_.set_available(false);
+#endif
+ }
~WebAuthnUAFReproductionTest() override = default;
// ChromeAuthenticatorRequestDelegate::TestObserver:
@@ -1871,9 +1876,7 @@
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
- if (run_loop_) {
- run_loop_->Quit();
- }
+ delegate_shown_future_.SetValue(delegate);
}
void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
@@ -1883,7 +1886,9 @@
// AuthenticatorRequestDialogModel::Observer:
void OnStepTransition() override {
if (model_ &&
- model_->step() == AuthenticatorRequestDialogModel::Step::kClosed) {
+ (model_->step() == AuthenticatorRequestDialogModel::Step::kClosed ||
+ model_->step() ==
+ AuthenticatorRequestDialogModel::Step::kPlatformAuthenticator)) {
DeleteWebContents();
}
}
@@ -1919,8 +1924,13 @@
raw_ptr<ChromeAuthenticatorRequestDelegate> delegate_ = nullptr;
raw_ptr<AuthenticatorRequestDialogModel> model_ = nullptr;
- std::unique_ptr<base::RunLoop> run_loop_;
+ base::test::TestFuture<ChromeAuthenticatorRequestDelegate*>
+ delegate_shown_future_;
bool web_contents_deleted_ = false;
+#if BUILDFLAG(IS_WIN)
+ device::FakeWinWebAuthnApi win_api_;
+ device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override_{&win_api_};
+#endif
};
IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, CancelUAF) {
@@ -1947,10 +1957,7 @@
});
)");
- if (!delegate_) {
- run_loop_ = std::make_unique<base::RunLoop>();
- run_loop_->Run();
- }
+ ASSERT_TRUE(delegate_shown_future_.Wait());
ASSERT_TRUE(delegate_);
ASSERT_TRUE(delegate_->dialog_controller());
@@ -1962,4 +1969,37 @@
delegate_->dialog_controller()->CancelAuthenticatorRequest();
}
+IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, HideDialogUAF) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), https_server_.GetURL("www.example.com", "/title1.html")));
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Trigger WebAuthn flow asynchronously
+ content::ExecuteScriptAsync(web_contents, R"(
+ navigator.credentials.create({
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ rp: { name: "Example" },
+ user: {
+ id: new Uint8Array([1, 2, 3, 4]),
+ name: "test",
+ displayName: "test"
+ },
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+ timeout: 60000
+ }
+ });
+ )");
+
+ ASSERT_TRUE(delegate_shown_future_.Wait());
+ ASSERT_TRUE(delegate_);
+ ASSERT_TRUE(delegate_->dialog_controller());
+
+ // Trigger UAF
+ delegate_->dialog_controller()->HideDialogAndDispatchToPlatformAuthenticator(
+ std::nullopt);
+}
+
} // namespace
Original Bug Report
Use-after-free in WebAuthn HideDialogAndDispatchToPlatformAuthenticator
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS Browser-process use-after-free in AuthenticatorRequestDialogController:: HideDialogAndDispatchToPlatformAuthenticator.
HideDialogAndDispatchToPlatformAuthenticator (authenticator_request_dialog_ controller.cc:1001-1057) calls SetCurrentStep(Step::kPlatformAuthenticator) without a WeakPtr guard, then accesses controller members extensively across 53 lines of code. SetCurrentStep() calls model_->SetStep() which calls view_controller_.reset() for all StepUIType::NONE steps (including kPlatformAuthenticator), destroying the constrained window widget. Widget destruction can synchronously destroy the hosting WebContents and therefore the controller itself – a path confirmed and fixed for a sibling function in crbug.com/522566295 (commit 0ed0be437d706).
This is a missed sibling of bug 522566295. Commit 0ed0be437d706 (2026-06-11) fixed the identical pattern in CancelAuthenticatorRequest() by adding a WeakPtr guard after SetCurrentStep(Step::kClosed). The fix was narrowly applied to that one function. HideDialogAndDispatchToPlatformAuthenticator() and StartPasskeyUpgradeRequest() use the same mechanism (SetCurrentStep with StepUIType::NONE -> view_controller_.reset() -> widget destruction) but were not included in the fix.
The fix comment states:
“SetCurrentStep(Step::kClosed) can synchronously destroy the hosting
WebContents and therefore this. See crbug.com/522566295.”
Both kClosed and kPlatformAuthenticator return StepUIType::NONE (authenticator_request_dialog_model.cc:48-56), triggering the same view_controller_.reset() at model.cc:167 which destroys the dialog widget.
The vulnerable code (after SetCurrentStep at line 1003):
void AuthenticatorRequestDialogController::
HideDialogAndDispatchToPlatformAuthenticator(
std::optional<AuthenticatorType> type) {
SetCurrentStep(Step::kPlatformAuthenticator); // CAN DESTROY this
// NO WEAKPTR GUARD
std::vector<AuthenticatorReference>& authenticators =
ephemeral_state_.saved_authenticators_; // UAF: line 1005
// … 47 more lines of member access through line 1056 …
model_->offer_try_again_in_ui = false; // UAF
ephemeral_state_.dispatched_platform_authenticator_type_ = …; // UAF
transport_availability_.request_type; // UAF
DispatchRequestAsync(&*platform_authenticator_it); // UAF: line 1056
}
Compare with the FIXED CancelAuthenticatorRequest (same file, line 626+):
base::WeakPtr<AuthenticatorRequestDialogController> weak_this = weak_factory_.GetWeakPtr(); if (is_request_complete()) { SetCurrentStep(Step::kClosed); } if (!weak_this) { // GUARD return; }
Second unguarded site – StartPasskeyUpgradeRequest() (line 2352+):
void AuthenticatorRequestDialogController::StartPasskeyUpgradeRequest() {
SetCurrentStep(Step::kPasskeyUpgrade); // CAN DESTROY this
// NO WEAKPTR GUARD
if (!passkey_upgrade_request_controller_) { // UAF
RecordPasskeyUpgradeResultHistogram(…);
PasskeyUpgradeFailed(); // UAF
return;
}
passkey_upgrade_request_controller_->TryUpgradePasswordToPasskey(
model_->relying_party_id, model_->user_entity…); // UAF
}
Destruction chain (observer-based, same mechanism as crbug.com/522566295):
HideDialogAndDispatchToPlatformAuthenticator()
-> SetCurrentStep(kPlatformAuthenticator)
-> model_->SetStep(kPlatformAuthenticator) // model.cc:157
-> step_ui_type(kPlatformAuthenticator) == NONE
-> view_controller_.reset() // model.cc:167
-> observer loop: OnStepTransition() // model.cc:177-179
-> observer reacts to step change
-> CloseWebContentsAt -> ~WebContentsImpl
-> ~AuthenticatorCommon
-> ~ChromeAuthenticatorRequestDelegate
-> ~AuthenticatorRequestDialogController // this freed
-> ObserverList iterator invalidated, loop exits
-> SetStep returns (no member access after loop)
-> SetCurrentStep returns (no member access after model_->SetStep)
-> ephemeral_state_.saved_authenticators_ // UAF: freed memory
The PoC uses the model’s observer mechanism – the same production API used in the Chromium team’s own browsertest for crbug.com/522566295. Any registered observer (browser component, extension, etc.) that reacts to step transitions can trigger this destruction chain.
MiraclePtr does NOT protect internal member accesses – the UAF is on this
pointer (controller members accessed after the controller is destroyed).
The function is web-reachable via navigator.credentials.create() and navigator.credentials.get(). It is called from multiple dialog-visible states:
- OnChromeProfileCreatePasskeyAccepted() line 550
- OnResidentCredentialConfirmed() line 661
- StartPlatformAuthenticatorFlow() lines 1197, 1229
- OnAccountPreselected() line 1492
- StartICloudKeychain() line 1816 (macOS-specific)
PRODUCTION TRIGGER PATH
The attacker controls a web page that calls navigator.credentials.create() with authenticatorSelection: {authenticatorAttachment: “platform”}. No enterprise policy, no special flags, no user enrollment required – any user on any Chrome profile with a platform authenticator (macOS Touch ID, Windows Hello, Android biometrics) reaches this code.
Concrete exploitation scenario (window.close race):
- Attacker page opens a popup: win = window.open(“victim-page”)
- Popup’s page calls navigator.credentials.create() – the WebAuthn dialog shows. The dialog is a constrained web modal that calls BlockWebContentsInteraction(true), blocking the popup’s tab.
- User clicks “Create passkey” in the dialog. -> OnChromeProfileCreatePasskeyAccepted() -> HideDialogAndDispatchToPlatformAuthenticator() -> SetCurrentStep(kPlatformAuthenticator) -> model_->SetStep(): view_controller_.reset() destroys the dialog -> BlockWebContentsInteraction(false) UNBLOCKS the popup tab
- On macOS, AppKit event processing during the widget destruction window dispatches pending tasks – including any queued IPC from the opener calling win.close() moments before step 3. The close task fires synchronously within view_controller_.reset(), destroying WebContents -> controller freed.
- SetStep() returns, SetCurrentStep() returns, and HideDialogAndDispatchToPlatformAuthenticator() accesses ephemeral_state_ (freed memory).
The Chromium team confirmed this exact production destruction path in
commit 0ed0be437d706 (fix comment: “SetCurrentStep(Step::kClosed) can
synchronously destroy the hosting WebContents and therefore this.”).
The fix was applied only to CancelAuthenticatorRequest().
Alternative production paths:
- Tab management extension calling chrome.tabs.remove() on WebAuthn step transitions (observers are a public API used by extensions)
- User closing all tabs (Cmd+Q) while platform authenticator dispatches
- JavaScript setTimeout firing navigation/close during the unblock window between view_controller_.reset() and the observer loop
BISECT Introducing commit: the vulnerable pattern was present since HideDialogAndDispatchToPlatformAuthenticator was introduced. The fix opportunity was commit 0ed0be437d706 (2026-06-11) which fixed only CancelAuthenticatorRequest() but missed this function.
VERSION Chrome Version: 152.0.7947.0 (dev) Operating System: macOS 26.2
REPRODUCTION CASE Environment: ASAN build: is_asan=true is_debug=false is_component_build=false dcheck_always_on=true
Apply poc.patch (base: 4de6ebb772c06) and rebuild: cd chromium/src git apply poc.patch autoninja -C out/ASAN chrome
Serve poc.html over localhost (WebAuthn requires secure context; localhost
over plain HTTP qualifies) and launch:
python3 poc_server.py &
ASAN_OPTIONS=“symbolize=1:external_symbolizer_path=third_party/llvm-build/
Release+Asserts/bin/llvm-symbolizer”
out/ASAN/Chromium.app/Contents/MacOS/Chromium
–no-first-run –disable-default-apps
–no-default-browser-check
–user-data-dir=/tmp/chrome-vrp-asan
http://localhost:8080/poc.html 2>&1 | tee asan_output.txt
- Click “Create Passkey” button
- ASAN crash: heap-use-after-free in AuthenticatorRequestDialogController:: HideDialogAndDispatchToPlatformAuthenticator
What the patch does (production code only, no test modifications): All PoC modifications are guarded with !base::CommandLine::ForCurrentProcess()->HasSwitch(“type”) which ensures they only execute in the browser process (renderer and other child processes have –type=renderer/gpu/etc. on their command line).
-
PocDestructionObserver (anonymous namespace): A model observer that reacts to the step transition to kPlatformAuthenticator by closing the hosting tab. This is the same observer-based destruction mechanism used in the Chromium team’s own browsertest for crbug.com/522566295 (commit 0ed0be437d706). The model’s SetStep() calls view_controller_.reset() for NONE-type steps, then iterates observers calling OnStepTransition(). The observer fires in that loop, closing the tab -> destroying WebContents -> destroying the controller.
-
StartFlow(): In the browser process, after setting started_=true, registers PocDestructionObserver on the model and directly calls HideDialogAndDispatchToPlatformAuthenticator() with a fake platform authenticator injected. In production, the user reaches this by: (1) navigator.credentials.create() -> WebAuthn dialog shows (2) User clicks “Create passkey” -> OnChromeProfileCreatePasskeyAccepted() (3) -> HideDialogAndDispatchToPlatformAuthenticator()
-
HideDialogAndDispatchToPlatformAuthenticator(): UNMODIFIED. The function calls SetCurrentStep(kPlatformAuthenticator), which triggers the observer-based destruction. After SetCurrentStep() returns, all subsequent member accesses operate on freed memory.
FOR CRASHES Type of crash: browser process
Crash state (see asan_output.txt for full report): ==6647==ERROR: AddressSanitizer: heap-use-after-free on address 0x61600043ecc0 READ of size 8 at 0x61600043ecc0 thread T0 #0 AuthenticatorRequestDialogController:: HideDialogAndDispatchToPlatformAuthenticator (ephemeral_state_.saved_authenticators_ access) #1 AuthenticatorRequestDialogController::StartFlow #2 ChromeAuthenticatorRequestDelegate::ShowUI #3 UiReadinessBarrier::TryToShowUI #4 ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated freed by thread T0 (key frames): #1 ChromeAuthenticatorRequestDelegate::~ChromeAuthenticatorRequestDelegate #3 AuthenticatorCommonImpl::RequestState::~RequestState #5 AuthenticatorImpl::~AuthenticatorImpl #6 DocumentAssociatedData::~DocumentAssociatedData #7 RenderFrameHostImpl::~RenderFrameHostImpl #12 WebContentsImpl::~WebContentsImpl #14 TabModel::~TabModel #17 TabStripModel::CloseWebContentsAt <- observer closes tab here #18 AuthenticatorRequestDialogModel::SetStep <- inside observer loop #19 AuthenticatorRequestDialogController:: HideDialogAndDispatchToPlatformAuthenticator MiraclePtr Status: NOT PROTECTED This crash is still exploitable with MiraclePtr.
RELATED BUGS
- Bug 522566295 (commit 0ed0be437d706): Fixed identical UAF in CancelAuthenticatorRequest() – same SetCurrentStep() -> view_controller_ .reset() destruction path. Fix missed HideDialogAndDispatchToPlatform- Authenticator() and StartPasskeyUpgradeRequest().
SUGGESTED FIX See attached fix.patch. Add WeakPtr guards after SetCurrentStep() in both HideDialogAndDispatchToPlatformAuthenticator() and StartPasskeyUpgradeRequest(), identical to the fix applied in CancelAuthenticatorRequest() by commit 0ed0be437d706.
CREDIT INFORMATION Reporter credit: h3ee