CVE-2026-10932
Overview
Files Changed
components/data_sharing/internal/android/data_sharing_sdk_delegate_android.cc
Patch
From 5ad288edd44880e1aaa448be42fbbc643dad0f78 Mon Sep 17 00:00:00 2001
From: Ritika Gupta <ritikagup@google.com>
Date: Thu, 07 May 2026 09:59:02 -0700
Subject: [PATCH] [DataSharing] Fix thread-safety and protobuf corruption in JNI callbacks
This CL fixes a potential memory corruption issue in the
CollaborationController by ensuring JNI error callbacks are routed to
the UI thread and correcting binary data transmission.
Changes:
- Thread Safety: Switched from TaskTraits.USER_VISIBLE to
TaskTraits.UI_USER_VISIBLE in all nine catch handlers within
DataSharingSDKDelegateBridge.java. This ensures error callbacks run on
the UI thread, maintaining the sequence affinity required by native
WeakPtr bindings.
- Data Integrity: Updated data_sharing_sdk_delegate_android.cc to pass
binary protobufs via ToJavaByteArray replacing ConvertUTF8ToJavaString
This prevents UTF-8 conversion errors that were causing deterministic
parse failures.
Bug: 501335606
Change-Id: I1f351f34ef533fdd7f22b6f9b87c8e85f5ecacdd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7822163
Commit-Queue: Ritika Gupta <ritikagup@google.com>
Reviewed-by: Siddhartha S <ssid@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627017}
---
diff --git a/components/data_sharing/internal/android/data_sharing_sdk_delegate_android.cc b/components/data_sharing/internal/android/data_sharing_sdk_delegate_android.cc
index 73ff7d3d..9e44afb 100644
--- a/components/data_sharing/internal/android/data_sharing_sdk_delegate_android.cc
+++ b/components/data_sharing/internal/android/data_sharing_sdk_delegate_android.cc
@@ -12,6 +12,8 @@
#include "base/android/scoped_java_ref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
+#include "base/task/bind_post_task.h"
+#include "base/task/single_thread_task_runner.h"
#include "components/data_sharing/internal/android/data_sharing_network_loader_android.h"
#include "components/data_sharing/internal/jni_headers/DataSharingSDKDelegateBridge_jni.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
@@ -22,6 +24,7 @@
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
+using base::android::ToJavaByteArray;
namespace data_sharing {
@@ -85,12 +88,13 @@
std::string create_group_params;
params.SerializeToString(&create_group_params);
std::unique_ptr<CreateGroupCallback> wrapped_callback =
- std::make_unique<CreateGroupCallback>(std::move(callback));
+ std::make_unique<CreateGroupCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_createGroup(
- env, java_obj_, ConvertUTF8ToJavaString(env, create_group_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, create_group_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunCreateGroupCallback.
wrapped_callback.release();
@@ -104,12 +108,13 @@
std::string read_groups_params;
params.SerializeToString(&read_groups_params);
std::unique_ptr<ReadGroupsCallback> wrapped_callback =
- std::make_unique<ReadGroupsCallback>(std::move(callback));
+ std::make_unique<ReadGroupsCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_readGroups(
- env, java_obj_, ConvertUTF8ToJavaString(env, read_groups_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, read_groups_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunReadGroupsCallback.
wrapped_callback.release();
@@ -125,12 +130,14 @@
std::string read_group_with_token_params;
params.SerializeToString(&read_group_with_token_params);
std::unique_ptr<ReadGroupsCallback> wrapped_callback =
- std::make_unique<ReadGroupsCallback>(std::move(callback));
+ std::make_unique<ReadGroupsCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_readGroupWithToken(
- env, java_obj_,
- ConvertUTF8ToJavaString(env, read_group_with_token_params), j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, read_group_with_token_params),
+ j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunReadGroupsCallback.
wrapped_callback.release();
@@ -144,12 +151,13 @@
std::string add_member_params;
params.SerializeToString(&add_member_params);
std::unique_ptr<GetStatusCallback> wrapped_callback =
- std::make_unique<GetStatusCallback>(std::move(callback));
+ std::make_unique<GetStatusCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_addMember(
- env, java_obj_, ConvertUTF8ToJavaString(env, add_member_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, add_member_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunAddMemberCallback.
wrapped_callback.release();
@@ -163,12 +171,13 @@
std::string remove_member_params;
params.SerializeToString(&remove_member_params);
std::unique_ptr<GetStatusCallback> wrapped_callback =
- std::make_unique<GetStatusCallback>(std::move(callback));
+ std::make_unique<GetStatusCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_removeMember(
- env, java_obj_, ConvertUTF8ToJavaString(env, remove_member_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, remove_member_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunRemoveMemberCallback.
wrapped_callback.release();
@@ -182,12 +191,13 @@
std::string leave_group_params;
params.SerializeToString(&leave_group_params);
std::unique_ptr<GetStatusCallback> wrapped_callback =
- std::make_unique<GetStatusCallback>(std::move(callback));
+ std::make_unique<GetStatusCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_leaveGroup(
- env, java_obj_, ConvertUTF8ToJavaString(env, leave_group_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, leave_group_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunDeleteGroupCallback.
wrapped_callback.release();
@@ -201,12 +211,13 @@
std::string delete_group_params;
params.SerializeToString(&delete_group_params);
std::unique_ptr<GetStatusCallback> wrapped_callback =
- std::make_unique<GetStatusCallback>(std::move(callback));
+ std::make_unique<GetStatusCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_deleteGroup(
- env, java_obj_, ConvertUTF8ToJavaString(env, delete_group_params),
- j_native_ptr);
+ env, java_obj_, ToJavaByteArray(env, delete_group_params), j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunDeleteGroupCallback.
wrapped_callback.release();
@@ -220,11 +231,13 @@
std::string lookup_gaid_id_params;
params.SerializeToString(&lookup_gaid_id_params);
std::unique_ptr<LookupGaiaIdByEmailCallback> wrapped_callback =
- std::make_unique<LookupGaiaIdByEmailCallback>(std::move(callback));
+ std::make_unique<LookupGaiaIdByEmailCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_lookupGaiaIdByEmail(
- env, java_obj_, ConvertUTF8ToJavaString(env, lookup_gaid_id_params),
+ env, java_obj_, ToJavaByteArray(env, lookup_gaid_id_params),
j_native_ptr);
// We expect Java to always call us back through
// JNI_DataSharingSDKDelegateBridge_RunLookupGaiaIdByEmailCallback.
@@ -239,11 +252,13 @@
std::string add_access_token_params;
params.SerializeToString(&add_access_token_params);
std::unique_ptr<AddAccessTokenCallback> wrapped_callback =
- std::make_unique<AddAccessTokenCallback>(std::move(callback));
+ std::make_unique<AddAccessTokenCallback>(
+ base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
+ std::move(callback)));
CHECK(wrapped_callback.get());
int64_t j_native_ptr = reinterpret_cast<int64_t>(wrapped_callback.get());
Java_DataSharingSDKDelegateBridge_addAccessToken(
- env, java_obj_, ConvertUTF8ToJavaString(env, add_access_token_params),
+ env, java_obj_, ToJavaByteArray(env, add_access_token_params),
j_native_ptr);
Original Bug Report
Potential Double-Free in CollaborationController via ThreadPool JNI Callback Race
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.
Overview: A data race leading to a potential Double-Free or Use-After-Free exists in the browser process’s CollaborationController. Binary protobufs passed to Java via UTF-8 conversion become corrupted, triggering an error handler that incorrectly posts a native callback to a background thread. Because thread-affinity checks compile out in Release builds, concurrent access to the state machine between the UI and background threads causes memory corruption.
Affected files:
components/data_sharing/internal/android/java/src/org/chromium/components/data_sharing/DataSharingSDKDelegateBridge.javacomponents/data_sharing/internal/android/data_sharing_sdk_delegate_android.cccomponents/collaboration/internal/collaboration_controller.cccomponents/data_sharing/internal/data_sharing_service_impl.ccchrome/browser/data_sharing/data_sharing_navigation_throttle.cccomponents/collaboration/internal/collaboration_controller.h
Estimated timestamp from git blame: 2025-03-19
Summary
A race condition leading to a potential Use-After-Free (UAF) or Double-Free exists in the browser process on Android. The vulnerability is triggered when binary protobuf data passed from C++ to Java is corrupted during UTF-8 conversion. The subsequent error handling in Java incorrectly dispatches the native callback to the ThreadPool instead of the UI thread. Because C++ base::WeakPtr thread affinity checks are disabled in Release builds, the CollaborationController state machine performs thread-unsafe transitions, leading to a data race on a std::unique_ptr and resulting in memory corruption.
Technical Description
1. Corrupted Protobuf Transmission
In data_sharing_sdk_delegate_android.cc, ReadGroupWithToken (and related methods) serializes request protobufs to a binary std::string and passes them to Java via ConvertUTF8ToJavaString. ConvertUTF8ToJavaString strictly enforces valid UTF-8 and replaces invalid sequences with the replacement character U+FFFD.
If an access_token is 128 bytes or larger, the protobuf length varint contains a high-bit byte (e.g., 0x80 0x01). Because 0x80 is a continuation byte without a valid lead byte, it is invalid UTF-8 and is replaced with U+FFFD. When Java attempts to re-encode this string back to bytes using String.getBytes(), U+FFFD converts to the 3-byte sequence [0xEF, 0xBF, 0xBD]. This irrevocably corrupts the binary protobuf, causing ReadGroupWithTokenParams.parseFrom() to throw an InvalidProtocolBufferException.
2. Incorrect Thread Dispatching
The Java catch handler for this exception in DataSharingSDKDelegateBridge.java invokes PostTask.postTask(TaskTraits.USER_VISIBLE, ...) to run the failure callback. On Android, TaskTraits.USER_VISIBLE dispatches to a base::ThreadPool worker thread rather than the UI thread where the flow originated.
3. Bypassed Thread Checks
The ThreadPool worker thread invokes the native callback, JNI_DataSharingSDKDelegateBridge_RunReadGroupsCallback, which resolves to DataSharingServiceImpl::OnReadSingleGroupCompleted and eventually AddingUserToGroupState::ProcessGroupDataOrFailureOutcome.
Both of these C++ callbacks are bound using base::WeakPtr. WeakPtr ensures safety against destroyed objects, but its sequence affinity enforcement relies on DCHECK_CALLED_ON_VALID_SEQUENCE. In standard Release builds, DCHECKs are compiled out. Consequently, execution silently proceeds on the background ThreadPool worker.
4. Memory Corruption
Inside ProcessGroupDataOrFailureOutcome, the code eventually calls controller_->TransitionTo(kError). This method reassigns the std::unique_ptr<ControllerState> current_state_ inside CollaborationController.
Because current_state_ is being reassigned from a ThreadPool worker without synchronization, it races with the UI thread. If the UI thread concurrently destroys the CollaborationController (e.g., if the user navigates away or closes the tab), a data race occurs between the std::unique_ptr’s assignment operator and its destructor. MiraclePtr (BRP) does not rewrite or protect the internal pointer of a std::unique_ptr, meaning this race directly leads to a Double-Free or UAF of the ControllerState object within the highly-privileged Browser process.
Potential Steps to Trigger
(Note: Our tooling agent does not have the ability to run code or build a working proof-of-concept; these are the suggested steps an attacker would follow to exploit the vulnerability.)
- An attacker creates a malicious link to a data sharing URL with a sufficiently long token (e.g.,
https://www.google.com/chrome/tabshare/?g=abc&t=[128+ bytes]). - The attacker tricks an Android user into clicking the link.
DataSharingNavigationThrottleintercepts the URL and starts theCollaborationControllerjoin flow on the UI thread.- The long token triggers the protobuf corruption, causing the Java exception handler to post the native callback to the ThreadPool.
- Concurrently, the attacker’s page triggers a navigation or closes the tab, causing the UI thread to destroy the
CollaborationController. - The concurrent state transition on the ThreadPool worker races with the destruction on the UI thread, causing a Double-Free in the Browser process.
Suggested Fix
- Correct Binary Data Passing: Stop using
ConvertUTF8ToJavaStringfor serialized protobufs. Instead, usebase::android::ToJavaByteArrayto pass the binary data accurately as abyte[]to Java. - Ensure Thread Safety for Callbacks: Wrap the native callbacks passed over JNI using
base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(), ...)before passing them to the SDK wrapper. This ensures that even if Java invokes the callback on a background thread, the C++ execution will always correctly hop back to the thread where the callback was created.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.