CVE-2026-11163
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
source_setcomponents/messages/android/BUILD.gn |
modified | |
message_enqueued_components/messages/android/message_wrapper.cc |
modified | |
is_in_queue_components/messages/android/message_wrapper.cc |
modified | |
ifcomponents/messages/android/message_wrapper.cc |
modified | |
MessageWrapperTestcomponents/messages/android/message_wrapper_unittest.cc |
modified | |
TEST_Fcomponents/messages/android/message_wrapper_unittest.cc |
modified |
Files Changed
components/BUILD.gncomponents/messages/android/BUILD.gncomponents/messages/android/java/src/org/chromium/components/messages/MessageWrapper.javacomponents/messages/android/message_dispatcher_bridge.cccomponents/messages/android/message_wrapper.cccomponents/messages/android/message_wrapper.hcomponents/messages/android/message_wrapper_unittest.cc
Patch
From 8e80975514e477664215843ab8e076c64d9dd994 Mon Sep 17 00:00:00 2001
From: Lijin Shen <lazzzis@google.com>
Date: Fri, 17 Apr 2026 12:29:59 -0700
Subject: [PATCH] [Messages] Clear native pointer in MessageWrapper destructor
This CL adds a call to `clearNativePtr` in the destructor of
`MessageWrapper` on the C++ side. This ensures that when the C++ object
is destroyed, the associated Java object has its native pointer set to
0.
Bug: 502072755
Change-Id: Ie2e7dda6352c0b3fd93bfabdd7653422769d8455
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7762195
Reviewed-by: Theresa Wellington <twellington@chromium.org>
Commit-Queue: Lijin Shen <lazzzis@google.com>
Cr-Commit-Position: refs/heads/main@{#1616775}
---
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 3731013e..34f7823d 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -705,6 +705,7 @@
"//components/media_router/browser/android:test_support_java",
"//components/messages/android:java",
"//components/messages/android:unit_tests",
+ "//components/messages/android/test:test_support_java",
"//components/metrics:metrics_java",
"//components/offline_items_collection/core:native_java_unittests_java",
"//components/one_time_tokens/android/backend/sms:java",
diff --git a/components/messages/android/BUILD.gn b/components/messages/android/BUILD.gn
index ddd00cf..5b350bf 100644
--- a/components/messages/android/BUILD.gn
+++ b/components/messages/android/BUILD.gn
@@ -115,11 +115,18 @@
source_set("unit_tests") {
testonly = true
- sources = [ "message_dispatcher_bridge_unittest.cc" ]
+ sources = [
+ "message_dispatcher_bridge_unittest.cc",
+ "message_wrapper_unittest.cc",
+ ]
deps = [
":android",
+ ":jni_headers",
+ ":test_support",
"//base",
+ "//base/test:test_support",
+ "//components/messages/android/test:test_support_cpp",
"//testing/gtest",
]
}
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageWrapper.java b/components/messages/android/java/src/org/chromium/components/messages/MessageWrapper.java
index 7df20583..11cd35e 100644
--- a/components/messages/android/java/src/org/chromium/components/messages/MessageWrapper.java
+++ b/components/messages/android/java/src/org/chromium/components/messages/MessageWrapper.java
@@ -231,6 +231,10 @@
mNativeMessageWrapper = 0;
}
+ long getNativePtrForTesting() {
+ return mNativeMessageWrapper;
+ }
+
@CalledByNative
Bitmap getIconBitmap() {
Drawable drawable = mMessageProperties.get(MessageBannerProperties.ICON);
diff --git a/components/messages/android/message_dispatcher_bridge.cc b/components/messages/android/message_dispatcher_bridge.cc
index 33d8490..af28a66 100644
--- a/components/messages/android/message_dispatcher_bridge.cc
+++ b/components/messages/android/message_dispatcher_bridge.cc
@@ -8,6 +8,7 @@
#include "base/android/jni_android.h"
#include "base/android/scoped_java_ref.h"
+#include "base/check.h"
#include "base/no_destructor.h"
#include "content/public/browser/web_contents.h"
@@ -43,6 +44,7 @@
content::WebContents* web_contents,
MessageScopeType scope_type,
MessagePriority priority) {
+ CHECK(!message->is_in_queue());
JNIEnv* env = base::android::AttachCurrentThread();
if (Java_MessageDispatcherBridge_enqueueMessage(
env, message->GetJavaMessageWrapper(),
@@ -59,6 +61,7 @@
MessageWrapper* message,
ui::WindowAndroid* window_android,
MessagePriority priority) {
+ CHECK(!message->is_in_queue());
JNIEnv* env = base::android::AttachCurrentThread();
if (Java_MessageDispatcherBridge_enqueueWindowScopedMessage(
env, message->GetJavaMessageWrapper(),
diff --git a/components/messages/android/message_wrapper.cc b/components/messages/android/message_wrapper.cc
index 33ba9c2..cba2166 100644
--- a/components/messages/android/message_wrapper.cc
+++ b/components/messages/android/message_wrapper.cc
@@ -25,7 +25,7 @@
DismissCallback dismiss_callback)
: action_callback_(std::move(action_callback)),
dismiss_callback_(std::move(dismiss_callback)),
- message_enqueued_(false) {
+ is_in_queue_(false) {
JNIEnv* env = base::android::AttachCurrentThread();
java_message_wrapper_ =
Java_MessageWrapper_create(env, reinterpret_cast<int64_t>(this),
@@ -33,7 +33,14 @@
}
MessageWrapper::~MessageWrapper() {
- CHECK(!message_enqueued_);
+ CHECK(!is_in_queue_);
+ if (java_message_wrapper_) {
+ // Clear the native pointer on the Java side in case the Java object
+ // outlives the C++ object, such as message_wrapper is created but never
+ // enqueued.
+ JNIEnv* env = base::android::AttachCurrentThread();
+ Java_MessageWrapper_clearNativePtr(env, java_message_wrapper_);
+ }
}
std::u16string MessageWrapper::GetTitle() {
@@ -259,7 +266,7 @@
void MessageWrapper::HandleDismissCallback(JNIEnv* env, int dismiss_reason) {
// Make sure message dismissed callback is called exactly once.
- message_enqueued_ = false;
+ is_in_queue_ = false;
Java_MessageWrapper_clearNativePtr(env, java_message_wrapper_);
if (!dismiss_callback_.is_null())
std::move(dismiss_callback_)
@@ -276,7 +283,7 @@
void MessageWrapper::SetMessageEnqueued(
const base::android::JavaRef<jobject>& java_window_android) {
- message_enqueued_ = true;
+ is_in_queue_ = true;
java_window_android_ = java_window_android;
JNIEnv* env = base::android::AttachCurrentThread();
Java_MessageWrapper_initializeSecondaryMenu(
diff --git a/components/messages/android/message_wrapper.h b/components/messages/android/message_wrapper.h
index 438002b..b0823fc 100644
--- a/components/messages/android/message_wrapper.h
+++ b/components/messages/android/message_wrapper.h
@@ -116,6 +116,8 @@
return java_window_android_;
}
+ bool is_in_queue() const { return is_in_queue_; }
+
const SkBitmap GetIconBitmap();
private:
@@ -125,7 +127,7 @@
SecondaryMenuItemSelectedCallback secondary_menu_item_selected_callback_;
DismissCallback dismiss_callback_;
// True if message is in queue.
- bool message_enqueued_;
+ bool is_in_queue_;
base::android::ScopedJavaGlobalRef<jobject> java_window_android_;
SecondaryMenuMaxSize secondary_menu_max_size_ = SecondaryMenuMaxSize::SMALL;
diff --git a/components/messages/android/message_wrapper_unittest.cc b/components/messages/android/message_wrapper_unittest.cc
new file mode 100644
index 0000000..7cb6c5e
--- /dev/null
+++ b/components/messages/android/message_wrapper_unittest.cc
@@ -0,0 +1,43 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/messages/android/message_wrapper.h"
+
+#include <memory>
+
+#include "base/android/jni_android.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/test/task_environment.h"
+#include "components/messages/android/test/messages_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace messages {
+
+class MessageWrapperTest : public ::testing::Test {
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ MessagesTestHelper helper_;
+};
+
+TEST_F(MessageWrapperTest, ClearsJavaPointerOnDestruction) {
+ // Create the MessageWrapper. MessageIdentifier::TEST_MESSAGE is used.
+ auto message_wrapper =
+ std::make_unique<MessageWrapper>(MessageIdentifier::TEST_MESSAGE);
+
+ // Keep a reference to the Java object before destroying the C++ object.
Regression Test / PoC
diff --git a/components/messages/android/message_wrapper_unittest.cc b/components/messages/android/message_wrapper_unittest.cc
new file mode 100644
index 0000000..7cb6c5e
--- /dev/null
+++ b/components/messages/android/message_wrapper_unittest.cc
@@ -0,0 +1,43 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/messages/android/message_wrapper.h"
+
+#include <memory>
+
+#include "base/android/jni_android.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/test/task_environment.h"
+#include "components/messages/android/test/messages_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace messages {
+
+class MessageWrapperTest : public ::testing::Test {
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ MessagesTestHelper helper_;
+};
+
+TEST_F(MessageWrapperTest, ClearsJavaPointerOnDestruction) {
+ // Create the MessageWrapper. MessageIdentifier::TEST_MESSAGE is used.
+ auto message_wrapper =
+ std::make_unique<MessageWrapper>(MessageIdentifier::TEST_MESSAGE);
+
+ // Keep a reference to the Java object before destroying the C++ object.
+ base::android::ScopedJavaGlobalRef<jobject> java_object;
+ java_object.Reset(base::android::AttachCurrentThread(),
+ message_wrapper->GetJavaMessageWrapper());
+
+ // Verify it is not 0 initially.
+ ASSERT_NE(0, helper_.GetNativePtr(java_object));
+
+ // Destroy the C++ object.
+ message_wrapper.reset();
+
+ // Verify that the Java pointer was cleared (set to 0).
+ EXPECT_EQ(0, helper_.GetNativePtr(java_object));
+}
+
+} // namespace messages
diff --git a/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java b/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
index 66a5e45..f843100 100644
--- a/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
+++ b/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
@@ -5,6 +5,7 @@
package org.chromium.components.messages;
import org.jni_zero.CalledByNative;
+import org.jni_zero.CalledByNativeForTesting;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
@@ -138,11 +139,17 @@
return messageStates.get(index).handler.getMessageIdentifier();
}
+ @CalledByNativeForTesting
+ public static long getNativePtr(MessageWrapper messageWrapper) {
+ return messageWrapper.getNativePtrForTesting();
+ }
+
/**
* Get the property model of a message.
+ *
* @param messageStateHandler The {@link MessageStateHandler} of an active message.
* @return The {@link PropertyModel} of a message if applicable. Currently supported
- * implementations include {@link SingleActionMessage}.
+ * implementations include {@link SingleActionMessage}.
*/
public static PropertyModel getCurrentMessage(MessageStateHandler messageStateHandler) {
assert messageStateHandler != null;
diff --git a/components/messages/android/test/messages_test_helper.cc b/components/messages/android/test/messages_test_helper.cc
index 52969f3..991f41e 100644
--- a/components/messages/android/test/messages_test_helper.cc
+++ b/components/messages/android/test/messages_test_helper.cc
@@ -4,6 +4,11 @@
#include "components/messages/android/test/messages_test_helper.h"
+#include <cstdint>
+
+#include "base/android/jni_android.h"
+#include "base/functional/callback.h"
+
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/messages/android/test/jni_headers/MessagesTestHelper_jni.h"
@@ -16,8 +21,7 @@
MessagesTestHelper::~MessagesTestHelper() = default;
-int messages::MessagesTestHelper::GetMessageCount(
- ui::WindowAndroid* window_android) {
+int MessagesTestHelper::GetMessageCount(ui::WindowAndroid* window_android) {
JNIEnv* env = jni_zero::AttachCurrentThread();
return Java_MessagesTestHelper_getMessageCount(
env, window_android->GetJavaObject());
@@ -30,6 +34,12 @@
env, window_android->GetJavaObject(), index);
}
+int64_t MessagesTestHelper::GetNativePtr(
+ const base::android::JavaRef<jobject>& message_wrapper) {
+ JNIEnv* env = jni_zero::AttachCurrentThread();
+ return Java_MessagesTestHelper_getNativePtr(env, message_wrapper);
+}
+
void MessagesTestHelper::AttachTestMessageDispatcherForTesting(
ui::WindowAndroid* window_android) {
JNIEnv* env = jni_zero::AttachCurrentThread();
diff --git a/components/messages/android/test/messages_test_helper.h b/components/messages/android/test/messages_test_helper.h
index 8057e5e..94c735de 100644
--- a/components/messages/android/test/messages_test_helper.h
+++ b/components/messages/android/test/messages_test_helper.h
@@ -7,6 +7,7 @@
#include <jni.h>
+#include "base/android/scoped_java_ref.h"
#include "base/functional/callback_forward.h"
#include "ui/android/window_android.h"
@@ -24,6 +25,7 @@
int GetMessageCount(ui::WindowAndroid* window_android);
int GetMessageIdentifier(ui::WindowAndroid* window_android, int index);
+ int64_t GetNativePtr(const base::android::JavaRef<jobject>& message_wrapper);
// Attach a test-only simplified message dispatcher to the window android.
// This is required to listen to events like message enqueued.
Original Bug Report
Use-After-Free in Android HaTS MessageWrapper via bypass of Java MessageDispatcher
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 Use-After-Free vulnerability exists in the Android Happiness Tracking Survey (HaTS) component. If a WebContents is destroyed while a survey is pending, the native MessageWrapper object is freed without clearing its Java counterpart’s native pointer, leading to a highly exploitable UAF when the survey is eventually interacted with.
Affected files:
chrome/browser/ui/android/hats/hats_service_android.cccomponents/messages/android/message_wrapper.ccchrome/browser/ui/android/hats/java/src/org/chromium/chrome/browser/ui/hats/MessageSurveyUiDelegate.javacomponents/messages/android/java/src/org/chromium/components/messages/MessageWrapper.java
Estimated timestamp from git blame: 2023-09-08
Summary
A precise Use-After-Free (UAF) vulnerability exists in the Android Happiness Tracking Survey (HaTS) component in the browser process.
When a survey is launched (e.g., following a permission prompt), a native C++ messages::MessageWrapper object is created alongside a corresponding Java peer. If the WebContents that triggered the survey is destroyed while the survey payload is asynchronously downloading, the DelayedSurveyTask correctly detects this and destroys the C++ MessageWrapper.
However, because the HaTS Java code enqueues survey messages by directly bypassing the C++ MessageDispatcherBridge, the message_enqueued_ safety flag remains false. This causes the C++ ~MessageWrapper() destructor to skip its safety CHECK and free the memory without clearing the dangling native pointer in the Java peer. When the survey banner is eventually displayed and the user interacts with it (or it auto-dismisses), the Java code invokes JNI callbacks using the dangling pointer, resulting in reliable Remote Code Execution (RCE).
Technical Details
-
Survey Creation:
HatsServiceAndroid::LaunchDelayedSurveyForWebContentscreates aDelayedSurveyTask. This task allocates amessages::MessageWrapper(C++) and invokes JNI to create a matching JavaMessageWrapperobject. The C++thispointer is cast to a rawlongand stored in the Java object’smNativeMessageWrapperfield. -
Asynchronous Download: The Java
SurveyClientImplinitiates an asynchronous download for the survey payload viaSurveyController.downloadSurvey(). During this time, the Java objects are kept alive by callbacks. -
WebContents Destruction: If the
WebContentsis destroyed (e.g., via script callingpopup.close()), theDelayedSurveyTask(aWebContentsObserver) detects this and removes itself viaHatsServiceAndroid::RemoveTask(). This destroys the C++MessageWrapper. -
Bypass of Safety Checks:
- The C++
~MessageWrapper()destructor assertsCHECK(!message_enqueued_);. - Normally,
MessageDispatcherBridgein C++ sets this flag totrue. - However,
MessageSurveyUiDelegate.javaexplicitly callsmMessageDispatcher.enqueueWindowScopedMessage(...)entirely on the Java side. The C++ bridge is bypassed, somessage_enqueued_remainsfalse. - The C++ destructor completes without crashing.
- The C++
-
Dangling Java Pointer: The C++ destructor frees the object memory, but fails to call
Java_MessageWrapper_clearNativePtr(). The JavaMessageWrapper.mNativeMessageWrapperremains populated with a pointer to freed memory. -
UAF and MiraclePtr Bypass: MiraclePtr provides no protection here because the pointer was passed across JNI as a raw
long; there is noraw_ptrto maintain the quarantine reference count. The memory is fully returned to the allocator. -
Exploitation: An attacker can spray the browser heap to reallocate this freed chunk. Once the survey download completes, the banner is displayed. When the user clicks the banner, Java executes
MessageWrapperJni.get().handleActionClick(mNativeMessageWrapper). This enters the C++ methodMessageWrapper::HandleActionClick, which invokesstd::move(action_callback_).Run(). By controlling the reallocated memory, the attacker controls thebase::OnceClosureand hijacks the vtable, achieving sandbox escape and full RCE in the browser process.
Potential Reproduction Steps (Conceptual)
- Host an attacker page that opens a popup window.
- In the popup, trigger a feature that probabilistically qualifies for a HaTS survey (e.g.,
navigator.geolocation.getCurrentPosition()). - The user accepts or denies the prompt, triggering the background survey download.
- The attacker script immediately calls
popup.close()to destroy theWebContentsbefore the download completes. - The attacker script performs a targeted heap spray in the browser process.
- The survey download completes, and the HaTS banner appears on the original opener tab.
- The user clicks the banner, triggering the UAF and executing the attacker’s payload.
Suggested Fix
The ~MessageWrapper() destructor must safely inform the Java peer that the native object is being destroyed, regardless of whether the message was enqueued or not. Add Java_MessageWrapper_clearNativePtr(env, java_message_wrapper_); inside the ~MessageWrapper() destructor to ensure the Java-side dangling pointer is always zeroed out upon destruction.
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.