Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Bug ClassUAF
Tracker513222854
Fix commit494ab52ccc31 (chromium/src) +40/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
switch
remoting/protocol/negotiating_client_authenticator.cc
modified
if
remoting/protocol/negotiating_client_authenticator.cc
modified

Files Changed

  • remoting/protocol/negotiating_authenticator_unittest.cc
  • remoting/protocol/negotiating_client_authenticator.cc
From 494ab52ccc31b1d6c24bff520d0af2f8205a843e Mon Sep 17 00:00:00 2001
From: Joe Downing <joedow@google.com>
Date: Fri, 15 May 2026 13:02:04 -0700
Subject: [PATCH] Fix UAF in NegotiatingClientAuthenticator when secret fetching is synchronous.

CreateAuthenticatorForCurrentMethod can result in the destruction of the NegotiatingClientAuthenticator instance if the secret fetching callback is executed synchronously and triggers teardown. This CL adds a WeakPtr check to prevent accessing this after destruction. Also
ensures ChainStateChangeAfterAcceptedWithUnderlying is called at the
point when `current_authenticator` changes.

TAG=agy
CONV=0c3fc16d-6be8-4701-a439-0662efc3b2f9
BUG=513222854

Change-Id: I6482828d15beb7248a95cdf941ada4de74fe4b75
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7851612
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Commit-Queue: Jamie Walch <jamiewalch@chromium.org>
Commit-Queue: Joe Downing <joedow@chromium.org>
Auto-Submit: Joe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1631490}
---

diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index eaf1b6e0..47e2313 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -436,4 +436,35 @@
   ASSERT_EQ(host, nullptr);
 }
 
+TEST_F(NegotiatingAuthenticatorTest,
+       CreateAuthenticatorForCurrentMethod_SynchronousRejectionTeardown) {
+  protocol::ClientAuthenticationConfig client_auth_config;
+  client_auth_config.host_id = kTestHostId;
+
+  std::unique_ptr<NegotiatingClientAuthenticator> client;
+
+  client_auth_config.fetch_secret_callback = base::BindRepeating(
+      [](std::unique_ptr<NegotiatingClientAuthenticator>* client_ptr,
+         bool pairing_supported,
+         const protocol::SecretFetchedCallback& secret_fetched_callback) {
+        secret_fetched_callback.Run(kTestPin);
+        client_ptr->reset();
+      },
+      &client);
+
+  client = std::make_unique<NegotiatingClientAuthenticator>(
+      kClientJid, kHostJid, client_auth_config);
+
+  // Transition state to WAITING_MESSAGE.
+  std::ignore = client->GetNextMessage();
+
+  JingleAuthentication host_message;
+  host_message.method = AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519;
+
+  // This should not crash.
+  client->ProcessMessage(host_message, base::DoNothing());
+
+  EXPECT_EQ(client, nullptr);
+}
+
 }  // namespace remoting::protocol
diff --git a/remoting/protocol/negotiating_client_authenticator.cc b/remoting/protocol/negotiating_client_authenticator.cc
index 678a3be..3167905 100644
--- a/remoting/protocol/negotiating_client_authenticator.cc
+++ b/remoting/protocol/negotiating_client_authenticator.cc
@@ -115,6 +115,7 @@
     base::OnceClosure resume_callback) {
   DCHECK_EQ(state(), PROCESSING_MESSAGE);
   DCHECK(current_method_ != AuthenticationMethod::INVALID);
+
   switch (current_method_) {
     case AuthenticationMethod::INVALID:
       NOTREACHED();
@@ -126,26 +127,31 @@
               base::BindRepeating(&Spake2Authenticator::CreateForClient,
                                   local_id_, remote_id_));
       current_authenticator_ = base::WrapUnique(pairing_authenticator);
+      ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
       pairing_authenticator->Start(preferred_initial_state,
                                    std::move(resume_callback));
       break;
     }
 
-    case AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519:
+    case AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519: {
+      auto weak_self = weak_factory_.GetWeakPtr();
       config_.fetch_secret_callback.Run(
           false,
           base::BindRepeating(
               &NegotiatingClientAuthenticator::CreateSharedSecretAuthenticator,
               weak_factory_.GetWeakPtr(), preferred_initial_state,
               base::Passed(std::move(resume_callback))));
+      if (!weak_self) {
+        return;
+      }
       break;
+    }
 
     case AuthenticationMethod::CLOUD_SESSION_AUTHZ_SPAKE2_CURVE25519:
     case AuthenticationMethod::CORP_SESSION_AUTHZ_SPAKE2_CURVE25519:
       NOTREACHED();
   }
 
-  ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
 }
 
 void NegotiatingClientAuthenticator::CreateSharedSecretAuthenticator(
@@ -157,6 +163,7 @@
 
   current_authenticator_ = Spake2Authenticator::CreateForClient(
       local_id_, remote_id_, shared_secret_hash, initial_state);
+  ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);
   std::move(resume_callback).Run();
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index eaf1b6e0..47e2313 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -436,4 +436,35 @@
   ASSERT_EQ(host, nullptr);
 }
 
+TEST_F(NegotiatingAuthenticatorTest,
+       CreateAuthenticatorForCurrentMethod_SynchronousRejectionTeardown) {
+  protocol::ClientAuthenticationConfig client_auth_config;
+  client_auth_config.host_id = kTestHostId;
+
+  std::unique_ptr<NegotiatingClientAuthenticator> client;
+
+  client_auth_config.fetch_secret_callback = base::BindRepeating(
+      [](std::unique_ptr<NegotiatingClientAuthenticator>* client_ptr,
+         bool pairing_supported,
+         const protocol::SecretFetchedCallback& secret_fetched_callback) {
+        secret_fetched_callback.Run(kTestPin);
+        client_ptr->reset();
+      },
+      &client);
+
+  client = std::make_unique<NegotiatingClientAuthenticator>(
+      kClientJid, kHostJid, client_auth_config);
+
+  // Transition state to WAITING_MESSAGE.
+  std::ignore = client->GetNextMessage();
+
+  JingleAuthentication host_message;
+  host_message.method = AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519;
+
+  // This should not crash.
+  client->ProcessMessage(host_message, base::DoNothing());
+
+  EXPECT_EQ(client, nullptr);
+}
+
 }  // namespace remoting::protocol
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential pre-auth Use-After-Free in NegotiatingClientAuthenticator

Project Fortify, 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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in NegotiatingClientAuthenticator when used with synchronous secret fetching, such as in the Boca Spotlight feature on ChromeOS. A malicious host can trigger synchronous object destruction during the authentication handshake, leading to memory corruption in the unsandboxed ash-chrome process. This issue is reachable pre-authentication via a crafted signaling message.

Affected files:

  • remoting/protocol/negotiating_client_authenticator.cc
  • remoting/protocol/negotiating_authenticator_base.h
  • remoting/protocol/authenticator.cc
  • remoting/protocol/authenticator.h

Estimated timestamp from git blame: 2024-02-23

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in NegotiatingClientAuthenticator::CreateAuthenticatorForCurrentMethod. The vulnerability occurs when the config_.fetch_secret_callback is executed synchronously, which is the case in the Boca Spotlight client implementation. A malicious host can craft a signaling message that causes the authenticator to be destroyed while its methods are still active on the execution stack.

Root Cause Analysis

In remoting/protocol/negotiating_client_authenticator.cc, the function CreateAuthenticatorForCurrentMethod invokes config_.fetch_secret_callback.Run(). In the Boca Spotlight feature, this callback is synchronous and immediately executes the provided SecretFetchedCallback.

An attacker acting as a malicious host can trigger the following synchronous call chain:

  1. The host sends a session-accept message that selects SHARED_SECRET_SPAKE2_CURVE25519 but lacks a required host certificate.
  2. NegotiatingClientAuthenticator::ProcessMessage initiates CreateAuthenticatorForCurrentMethod.
  3. The synchronous fetch_secret_callback creates a Spake2Authenticator and runs its ProcessMessage method.
  4. Spake2Authenticator synchronously rejects the connection due to the missing certificate.
  5. This rejection triggers a synchronous state change in JingleSession, which notifies RemotingClient.
  6. RemotingClient::StopSession is called, which executes connection_.reset(), synchronously destroying the WebrtcConnectionToHost, the JingleSession, and the NegotiatingClientAuthenticator (this).

Upon returning from the callback in CreateAuthenticatorForCurrentMethod, the code attempts to execute line 148:

ChainStateChangeAfterAcceptedWithUnderlying(*current_authenticator_);

At this point, this has been freed, leading to a UAF read of the current_authenticator_ pointer and a subsequent UAF write as a base::RepeatingClosure is assigned into the memory of the already-freed sub-authenticator.

Potential Impact

This vulnerability allows for memory corruption in the unsandboxed ash-chrome browser process on ChromeOS. As a pre-authentication vulnerability reachable via signaling messages, it represents a significant security risk. Successful exploitation could lead to remote code execution (RCE) with the privileges of the browser process.

Suggested Attack Steps

An attacker would likely follow these steps to trigger the issue:

  1. Compromise a device that can act as a host in a Boca Spotlight session (e.g., a student device).
  2. Wait for a teacher device to initiate a Spotlight session.
  3. Respond to the session-initiate message with a crafted session-accept message that specifies SPAKE2 authentication but omits the certificate field.
  4. The teacher’s client process should then encounter the UAF during the synchronous rejection handling.

To mitigate this risk, NegotiatingClientAuthenticator should ensure it remains valid after the callback returns. Using a base::WeakPtr to check if this is still valid before dereferencing members or, ideally, ensuring that the fetch_secret_callback always executes asynchronously would prevent the synchronous teardown from occurring while the object is on the stack.

Note: These findings are based on code analysis and have not been verified with a functional proof-of-concept.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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.

View on issue tracker