CVE-2026-76017
Overview
Background
- Chromoting / remoting host
- Chrome’s Remote Desktop host code (
remoting/) that authenticates and serves an incoming remote client over a Jingle-based protocol. - `NegotiatingHostAuthenticator`
- the host-side authenticator that picks a concrete auth method (SPAKE2 shared-secret, paired, or SessionAuthz) and drives the challenge/response state machine for a session.
- `resume_callback` (`base::OnceClosure`)
- a continuation passed into a sub-authenticator’s
Start/Initialize; when the method setup can complete or fail immediately it may be run synchronously, before control returns to the caller. - `WeakPtr` self-guard
- the Chromium idiom of taking
weak_factory_.GetWeakPtr()before invoking a callback that may destroythis, then checking the pointer afterward to detect self-destruction and bail out.
Root Cause Analysis
In NegotiatingHostAuthenticator::CreateAuthenticator (named in the commit subject; the @@ hunk header only shows the base::OnceClosure resume_callback) parameter line, not the enclosing function), the code created a concrete authenticator and invoked its Start/Initialize with std::move(resume_callback), then, after the switch, dereferenced the member current_authenticator_ via ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_). The violated invariant is that this must still be alive when that trailing member access runs: because resume_callback can execute synchronously inside Start/Initialize, it can drive the session to closure and destroy the NegotiatingHostAuthenticator instance before the callback returns, leaving the subsequent *current_authenticator_ access reading freed memory (a use-after-free).
The fix captures auto weak_this = weak_factory_.GetWeakPtr(); at function entry and inserts if (!weak_this) { return; } immediately before the ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_) call, so a self-destroying callback causes an early return instead of a dangling dereference. As a companion correctness change, each branch now assigns current_authenticator_ = std::move(authenticator) before calling Start/Initialize through a saved raw pointer (auth_ptr), so ownership is settled before the callback can run rather than being written back into a member of an already-freed object.
resume_callback) that can synchronously destroy this, then unconditionally touching the member current_authenticator_ afterward; the fix detects that self-destruction with a pre-captured weak_this and returns early before the member is dereferenced.Attack Path
- Initiate a host authentication
A remote client connects to a Chromoting host and sends a
JingleAuthenticationmessage selecting an offered method (e.g.SHARED_SECRET_SPAKE2_CURVE25519,PAIRED_SPAKE2_CURVE25519, orCLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519), driving the host intoNegotiatingHostAuthenticator::CreateAuthenticator. - Force a synchronous continuation
Setup for the chosen method completes or fails immediately, so
resume_callbackruns synchronously insideStart/Initialize; the shippedSessionAuthzregression test models this withGenerateHostTokenreturningPERMISSION_DENIEDsynchronously, and the shared-secret/pairing tests do it via aSynchronousPairingRegistry. - Trigger session teardown from the callback
The synchronous
resume_callbackdrives the session to closure, which destroys theNegotiatingHostAuthenticator(this) — the regression tests reproduce this precisely with a callback of[&]() { host.reset(); }. - Dereference freed memory
Control returns from
Start/InitializeintoCreateAuthenticator, which (pre-patch) executesChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_), readingcurrent_authenticator_out of the now-freedthis— the use-after-free. - Downstream escalation
Any read/write primitive, controlled reallocation of the freed region, or code execution arising from the freed access is not shown by the diff and is
(inferred).
Impact Assessment
resume_callback runs synchronously and tears down the NegotiatingHostAuthenticator before the trailing current_authenticator_ dereference. The advisory classifies this as a use-after-free in Chromoting and states no more; it does not report exploited-in-the-wild status, and any progression from the freed-memory access to information disclosure, memory corruption, or code execution is (inferred) rather than demonstrated by the patch. The precondition is that setup for the negotiated method resolves synchronously and closes the session in the same call stack, as exercised by the three shipped SynchronousTeardown tests.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchremoting/protocol/negotiating_host_authenticator.cc |
modified | |
ifremoting/protocol/negotiating_host_authenticator.cc |
modified | |
FakeSessionAuthzServiceClientFactoryremoting/protocol/negotiating_authenticator_unittest.cc |
modified | |
NegotiatingAuthenticatorTestremoting/protocol/negotiating_authenticator_unittest.cc |
modified | |
BindLambdaForTestingremoting/protocol/negotiating_authenticator_unittest.cc |
modified |
Files Changed
remoting/protocol/negotiating_host_authenticator.ccremoting/protocol/negotiating_authenticator_unittest.cc
Audit Directions
- Callback-then-member-accessAudit any method that invokes a
base::OnceClosure/base::RepeatingCallback(especially resume/continue callbacks) and then touchesthismembers or calls member functions afterward; require aWeakPtrself-guard whenever the callback can synchronously destroy the object. - Synchronous-completion pathsLook for authenticators, registries, and service clients (e.g.
SessionAuthzServiceClient,PairingRegistry) whoseStart/Initialize/Generate*operations may invoke their completion callback inline rather than posting it, since these turn “later” teardown into same-stack destruction. - Assign-after-use orderingFlag places that move a locally-owned object into a member after invoking a callback on it, where the member write could land in freed storage; prefer assigning ownership first and calling through a saved raw pointer, as this fix does.
Patch
From 02bd649092f62e8416f02eca484578c20e88ba7d Mon Sep 17 00:00:00 2001
From: Joe Downing <joedow@google.com>
Date: Fri, 14 Aug 2026 10:28:51 -0700
Subject: [PATCH] Fix potential UAF in NegotiatingHostAuthenticator::CreateAuthenticator
During authentication setup, synchronous execution of resume_callback in NegotiatingHostAuthenticator::CreateAuthenticator can result in session closure and destruction of the NegotiatingHostAuthenticator instance. Upon returning from the callback, accessing current_authenticator_ causes a Use-After-Free.
This CL captures a WeakPtr before invoking the callback and returns early if this was destroyed.
TAG=agy
CONV=ec6a5028-dd53-4091-8c6b-7a88a373cc3a
Bug: 522819252
Change-Id: I9aa68827d17dfbf6a0a9ae972c9bb8e3acbe0b11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8261393
Commit-Queue: Joe Downing <joedow@chromium.org>
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1679658}
---
diff --git a/remoting/protocol/negotiating_host_authenticator.cc b/remoting/protocol/negotiating_host_authenticator.cc
index 69dbe5a..0d461033 100644
--- a/remoting/protocol/negotiating_host_authenticator.cc
+++ b/remoting/protocol/negotiating_host_authenticator.cc
@@ -134,6 +134,8 @@
base::OnceClosure resume_callback) {
DCHECK(current_method_ != AuthenticationMethod::INVALID);
+ auto weak_this = weak_factory_.GetWeakPtr();
+
switch (current_method_) {
case AuthenticationMethod::INVALID:
NOTREACHED();
@@ -147,8 +149,9 @@
base::BindRepeating(&Spake2Authenticator::CreateForHost, local_id_,
remote_id_, config_->local_cert,
config_->key_pair));
- authenticator->Start(std::move(resume_callback));
+ SessionAuthzAuthenticator* auth_ptr = authenticator.get();
current_authenticator_ = std::move(authenticator);
+ auth_ptr->Start(std::move(resume_callback));
break;
}
@@ -161,22 +164,23 @@
base::BindRepeating(&Spake2Authenticator::CreateForHost, local_id_,
remote_id_, config_->local_cert,
config_->key_pair));
- authenticator->Start(std::move(resume_callback));
+ SessionAuthzAuthenticator* auth_ptr = authenticator.get();
current_authenticator_ = std::move(authenticator);
+ auth_ptr->Start(std::move(resume_callback));
break;
}
case AuthenticationMethod::PAIRED_SPAKE2_CURVE25519: {
- PairingHostAuthenticator* pairing_authenticator =
- new PairingHostAuthenticator(
- config_->pairing_registry,
- base::BindRepeating(&Spake2Authenticator::CreateForHost,
- local_id_, remote_id_, config_->local_cert,
- config_->key_pair),
- config_->shared_secret_hash);
- current_authenticator_.reset(pairing_authenticator);
- pairing_authenticator->Initialize(client_id_, preferred_initial_state,
- std::move(resume_callback));
+ auto pairing_authenticator = std::make_unique<PairingHostAuthenticator>(
+ config_->pairing_registry,
+ base::BindRepeating(&Spake2Authenticator::CreateForHost, local_id_,
+ remote_id_, config_->local_cert,
+ config_->key_pair),
+ config_->shared_secret_hash);
+ PairingHostAuthenticator* auth_ptr = pairing_authenticator.get();
+ current_authenticator_ = std::move(pairing_authenticator);
+ auth_ptr->Initialize(client_id_, preferred_initial_state,
+ std::move(resume_callback));
break;
}
@@ -188,6 +192,10 @@
break;
}
+ if (!weak_this) {
+ return;
+ }
+
ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
}
diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index 47e2313..f7f62c2 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -9,9 +9,12 @@
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
+#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "net/base/net_errors.h"
+#include "remoting/base/mock_session_authz_service_client.h"
#include "remoting/base/rsa_key_pair.h"
+#include "remoting/base/session_authz_service_client_factory.h"
#include "remoting/protocol/auth_util.h"
#include "remoting/protocol/authenticator.h"
#include "remoting/protocol/authenticator_test_base.h"
@@ -104,6 +107,28 @@
raw_ptr<Authenticator> authenticator_;
};
+class FakeSessionAuthzServiceClientFactory
+ : public SessionAuthzServiceClientFactory {
+ public:
+ FakeSessionAuthzServiceClientFactory(
+ AuthenticationMethod method,
+ std::unique_ptr<SessionAuthzServiceClient> client)
+ : method_(method), client_(std::move(client)) {}
+
+ std::unique_ptr<SessionAuthzServiceClient> Create() override {
+ return std::move(client_);
+ }
+
+ AuthenticationMethod method() override { return method_; }
+
+ protected:
+ ~FakeSessionAuthzServiceClientFactory() override = default;
+
+ private:
+ AuthenticationMethod method_;
+ std::unique_ptr<SessionAuthzServiceClient> client_;
+};
+
} // namespace
class NegotiatingAuthenticatorTest : public AuthenticatorTestBase {
@@ -467,4 +492,74 @@
EXPECT_EQ(client, nullptr);
}
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_SharedSecret_SynchronousTeardown) {
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddSharedSecretAuth("hash");
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method = AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_Pairing_SynchronousTeardown) {
+ auto pairing_registry = base::MakeRefCounted<SynchronousPairingRegistry>(
+ std::make_unique<MockPairingRegistryDelegate>());
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddPairingAuth(pairing_registry);
+ auth_config->AddSharedSecretAuth("hash");
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method = AuthenticationMethod::PAIRED_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_SessionAuthz_SynchronousTeardown) {
+ auto mock_client = std::make_unique<MockSessionAuthzServiceClient>();
+ EXPECT_CALL(*mock_client, GenerateHostToken(_))
+ .WillOnce([](MockSessionAuthzServiceClient::GenerateHostTokenCallback
+ callback) {
+ std::move(callback).Run(
+ HttpStatus(HttpStatus::Code::PERMISSION_DENIED, "denied"), nullptr);
+ });
+
+ auto factory = base::MakeRefCounted<FakeSessionAuthzServiceClientFactory>(
+ AuthenticationMethod::CLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519,
+ std::move(mock_client));
+
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddSessionAuthzAuth(factory);
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method =
+ AuthenticationMethod::CLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
} // namespace remoting::protocol
Regression Test / PoC
diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index 47e2313..f7f62c2 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -9,9 +9,12 @@
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
+#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "net/base/net_errors.h"
+#include "remoting/base/mock_session_authz_service_client.h"
#include "remoting/base/rsa_key_pair.h"
+#include "remoting/base/session_authz_service_client_factory.h"
#include "remoting/protocol/auth_util.h"
#include "remoting/protocol/authenticator.h"
#include "remoting/protocol/authenticator_test_base.h"
@@ -104,6 +107,28 @@
raw_ptr<Authenticator> authenticator_;
};
+class FakeSessionAuthzServiceClientFactory
+ : public SessionAuthzServiceClientFactory {
+ public:
+ FakeSessionAuthzServiceClientFactory(
+ AuthenticationMethod method,
+ std::unique_ptr<SessionAuthzServiceClient> client)
+ : method_(method), client_(std::move(client)) {}
+
+ std::unique_ptr<SessionAuthzServiceClient> Create() override {
+ return std::move(client_);
+ }
+
+ AuthenticationMethod method() override { return method_; }
+
+ protected:
+ ~FakeSessionAuthzServiceClientFactory() override = default;
+
+ private:
+ AuthenticationMethod method_;
+ std::unique_ptr<SessionAuthzServiceClient> client_;
+};
+
} // namespace
class NegotiatingAuthenticatorTest : public AuthenticatorTestBase {
@@ -467,4 +492,74 @@
EXPECT_EQ(client, nullptr);
}
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_SharedSecret_SynchronousTeardown) {
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddSharedSecretAuth("hash");
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method = AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_Pairing_SynchronousTeardown) {
+ auto pairing_registry = base::MakeRefCounted<SynchronousPairingRegistry>(
+ std::make_unique<MockPairingRegistryDelegate>());
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddPairingAuth(pairing_registry);
+ auth_config->AddSharedSecretAuth("hash");
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method = AuthenticationMethod::PAIRED_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
+TEST_F(NegotiatingAuthenticatorTest,
+ CreateHostAuthenticator_SessionAuthz_SynchronousTeardown) {
+ auto mock_client = std::make_unique<MockSessionAuthzServiceClient>();
+ EXPECT_CALL(*mock_client, GenerateHostToken(_))
+ .WillOnce([](MockSessionAuthzServiceClient::GenerateHostTokenCallback
+ callback) {
+ std::move(callback).Run(
+ HttpStatus(HttpStatus::Code::PERMISSION_DENIED, "denied"), nullptr);
+ });
+
+ auto factory = base::MakeRefCounted<FakeSessionAuthzServiceClientFactory>(
+ AuthenticationMethod::CLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519,
+ std::move(mock_client));
+
+ auto auth_config =
+ std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_);
+ auth_config->AddSessionAuthzAuth(factory);
+ auto host = std::make_unique<NegotiatingHostAuthenticator>(
+ kHostJid, kClientJid, std::move(auth_config));
+
+ JingleAuthentication client_message;
+ client_message.method =
+ AuthenticationMethod::CLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519;
+
+ // This should not crash.
+ host->ProcessMessage(client_message,
+ base::BindLambdaForTesting([&]() { host.reset(); }));
+
+ EXPECT_EQ(host, nullptr);
+}
+
} // namespace remoting::protocol
Original Bug Report
Potential Use-After-Free in NegotiatingHostAuthenticator::CreateAuthenticator
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A pre-authentication Use-After-Free (UAF) vulnerability exists in Chrome Remote Desktop’s NegotiatingHostAuthenticator. During authentication setup, an invalid initial message can synchronously trigger session failure and object destruction during a nested callback. Upon returning from the callback, execution attempts to access members of the now-freed authenticator instance, creating an RCE primitive.
Affected files:
remoting/protocol/negotiating_host_authenticator.cc
Estimated timestamp from git blame: 2024-02-23
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in NegotiatingHostAuthenticator::CreateAuthenticator within the Chrome Remote Desktop host (remoting_host). The vulnerability is triggered when an incoming authentication message is synchronously rejected during the initialization of an underlying authenticator. This synchronous rejection forces the immediate closure of the Jingle session, which deletes the NegotiatingHostAuthenticator instance. Execution then proceeds back up the stack to access a member variable of the freed this pointer.
Technical Details
In remoting/protocol/negotiating_host_authenticator.cc, the CreateAuthenticator method initializes underlying authenticators. For several negotiation paths, a resume_callback is executed synchronously before the function returns. If the execution of this callback results in the destruction of the session, a UAF occurs.
Below is a potential step-by-step execution flow an attacker could use to trigger the vulnerability. (Note: These are suggested steps based on static code analysis, as we do not have an actively running exploit proof-of-concept).
- An unauthenticated attacker connects to the host and sends a
session-initiateJingle signaling message. - In this message, they request the
SHARED_SECRET_SPAKE2_CURVE25519method, but intentionally provide a malformed payload (e.g., an empty<spake-message>). JingleSession::AcceptIncomingConnectionbegins processing the request and passes the message to a newly createdNegotiatingHostAuthenticator.NegotiatingHostAuthenticator::ProcessMessageselects the attacker’s preferred method and callsCreateAuthenticator(), passing aresume_callbackthat wrapsProcessMessageInternal.CreateAuthenticatorhandles theSHARED_SECRET_SPAKE2_CURVE25519path (lines 183-188). It instantiatesSpake2Authenticator::CreateForHostand assigns it tothis->current_authenticator_.- At line 187, it synchronously invokes the callback:
std::move(resume_callback).Run();. - This executes
ProcessMessageInternal, which passes the malformed payload to the underlyingSpake2Authenticator::ProcessMessage. Spake2Authenticatorencounters the empty<spake-message>, sets its state toREJECTED, and fires its own state-update callbacks.- This rapidly propagates the
REJECTEDstate back toJingleSession::ContinueAcceptIncomingConnection, which immediately invokesClose(). Close()triggers a session failure cascade:ClientSession::OnConnectionClosedis called, which notifiesChromotingHost::OnSessionClosed.ChromotingHostsynchronously erases theClientSessionfrom its internal map, which deletes theJingleSessionobject and, finally, theNegotiatingHostAuthenticator(this).- The call stack unwinds back to
NegotiatingHostAuthenticator::CreateAuthenticatorat line 191:
191: ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
- The execution dereferences
this->current_authenticator_on the freed object. This attempts to write abase::RepeatingCallbackto freed memory insideAuthenticator::ChainStateChangeAfterAcceptedWithUnderlying, providing a robust primitive for memory corruption and potential Remote Code Execution (RCE) in the unsandboxed host process.
Suggested Fix
Similar to how a previous vulnerability was fixed in the client-side equivalent (NegotiatingClientAuthenticator), NegotiatingHostAuthenticator::CreateAuthenticator should check if the object is still alive after invoking the resume_callback.
This can be safely implemented using base::WeakPtr:
auto weak_this = weak_factory_.GetWeakPtr();
std::move(resume_callback).Run();
if (!weak_this) {
return;
}
ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.