CVE-2026-87640
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
GinJavaMethodInvocationHelperTestcontent/browser/android/java/gin_java_method_invocation_helper_unittest.cc |
modified | |
TEST_Fcontent/browser/android/java/gin_java_method_invocation_helper_unittest.cc |
modified |
Files Changed
content/browser/android/java/gin_java_method_invocation_helper_unittest.cccontent/browser/android/java/gin_java_script_to_java_types_coercion.cccontent/browser/android/java/gin_java_script_to_java_types_coercion.h
Patch
From 9eb436e816e429d77ec159ffa01269b60de04d8c Mon Sep 17 00:00:00 2001
From: Peter E Conn <peconn@google.com>
Date: Tue, 23 Jun 2026 01:26:09 -0700
Subject: [PATCH] [Android] Fix heap buffer overflow in GinJavaBridgeValue deserialization
A compromised renderer could trigger a heap buffer overflow in the host
application process by sending a malformed BinaryValue through the
GinJavaBridgeRemoteObject.InvokeMethod Mojo interface. The validation
for the BinaryValue previously only existed inside a DCHECK, meaning it
was compiled out of release builds.
This CL replaces the DCHECK with a runtime validation check. If the
received BinaryValue is malformed, we now immediately call
`mojo::ReportBadMessage` to terminate the compromised renderer process,
preventing any further exploitation attempts.
Fix: 498482618
Credit: c6eed09fc8b174b0f3eebedcceb1e792
TAG=agy
CONV=a5bf8dab-1e07-4deb-ac4d-4f4b8676d338
Change-Id: Iaec8dcf3c0ae369f01c01a3d74881272fc596186
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7957501
Commit-Queue: Peter Conn <peconn@chromium.org>
Reviewed-by: Richard Coles <torne@chromium.org>
Reviewed-by: Ashley Newson <ashleynewson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1650842}
---
diff --git a/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc b/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
index 30f0573..fe63cab 100644
--- a/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
+++ b/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
@@ -11,7 +11,11 @@
#include "base/android/jni_android.h"
#include "base/values.h"
+#include "base/test/task_environment.h"
+#include "content/browser/android/java/gin_java_script_to_java_types_coercion.h"
#include "content/common/android/gin_java_bridge_value.h"
+#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
@@ -73,6 +77,8 @@
} // namespace
class GinJavaMethodInvocationHelperTest : public testing::Test {
+ private:
+ base::test::SingleThreadTaskEnvironment task_environment_;
};
namespace {
@@ -336,4 +342,25 @@
helper->GetInvocationError());
}
+TEST_F(GinJavaMethodInvocationHelperTest, MalformedBinaryValueKillsRenderer) {
+ // Create a malformed BinaryValue (only 4 bytes, header requires 12).
+ std::vector<uint8_t> bad_data(4, 0);
+ base::Value bad_value(bad_data);
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ JavaType target_type = JavaType::CreateFromBinaryName("java.lang.Object");
+ ObjectRefs object_refs;
+ mojom::GinJavaBridgeError error =
+ mojom::GinJavaBridgeError::kGinJavaBridgeNoError;
+
+ mojo::FakeMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ CoerceJavaScriptValueToJavaValue(env, bad_value, target_type, true,
+ object_refs, &error);
+
+ EXPECT_EQ("Malformed GinJavaBridgeValue",
+ bad_message_observer.WaitForBadMessage());
+}
+
} // namespace content
diff --git a/content/browser/android/java/gin_java_script_to_java_types_coercion.cc b/content/browser/android/java/gin_java_script_to_java_types_coercion.cc
index 2815bd8..520c298 100644
--- a/content/browser/android/java/gin_java_script_to_java_types_coercion.cc
+++ b/content/browser/android/java/gin_java_script_to_java_types_coercion.cc
@@ -22,6 +22,7 @@
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "content/common/android/gin_java_bridge_value.h"
+#include "mojo/public/cpp/bindings/message.h"
using base::android::ConvertUTF8ToJavaString;
using base::android::ScopedJavaLocalRef;
@@ -689,7 +690,11 @@
bool coerce_to_string,
const ObjectRefs& object_refs,
mojom::GinJavaBridgeError* error) {
- DCHECK(GinJavaBridgeValue::ContainsGinJavaBridgeValue(&value));
+ if (!GinJavaBridgeValue::ContainsGinJavaBridgeValue(&value)) {
+ mojo::ReportBadMessage("Malformed GinJavaBridgeValue");
+ *error = mojom::GinJavaBridgeError::kGinJavaBridgeNonAssignableTypes;
+ return jvalue();
+ }
std::unique_ptr<const GinJavaBridgeValue> gin_value(
GinJavaBridgeValue::FromValue(&value));
switch (gin_value->GetType()) {
diff --git a/content/browser/android/java/gin_java_script_to_java_types_coercion.h b/content/browser/android/java/gin_java_script_to_java_types_coercion.h
index a86e14f..ad843ce 100644
--- a/content/browser/android/java/gin_java_script_to_java_types_coercion.h
+++ b/content/browser/android/java/gin_java_script_to_java_types_coercion.h
@@ -12,13 +12,14 @@
#include "content/browser/android/java/gin_java_bound_object.h"
#include "content/browser/android/java/java_type.h"
#include "content/common/android/gin_java_bridge_errors.h"
+#include "content/common/content_export.h"
namespace content {
typedef std::map<GinJavaBoundObject::ObjectID, JavaObjectWeakGlobalRef>
ObjectRefs;
-jvalue CoerceJavaScriptValueToJavaValue(JNIEnv* env,
+CONTENT_EXPORT jvalue CoerceJavaScriptValueToJavaValue(JNIEnv* env,
const base::Value& value,
const JavaType& target_type,
bool coerce_to_string,
Regression Test / PoC
diff --git a/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc b/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
index 30f0573..fe63cab 100644
--- a/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
+++ b/content/browser/android/java/gin_java_method_invocation_helper_unittest.cc
@@ -11,7 +11,11 @@
#include "base/android/jni_android.h"
#include "base/values.h"
+#include "base/test/task_environment.h"
+#include "content/browser/android/java/gin_java_script_to_java_types_coercion.h"
#include "content/common/android/gin_java_bridge_value.h"
+#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
@@ -73,6 +77,8 @@
} // namespace
class GinJavaMethodInvocationHelperTest : public testing::Test {
+ private:
+ base::test::SingleThreadTaskEnvironment task_environment_;
};
namespace {
@@ -336,4 +342,25 @@
helper->GetInvocationError());
}
+TEST_F(GinJavaMethodInvocationHelperTest, MalformedBinaryValueKillsRenderer) {
+ // Create a malformed BinaryValue (only 4 bytes, header requires 12).
+ std::vector<uint8_t> bad_data(4, 0);
+ base::Value bad_value(bad_data);
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ JavaType target_type = JavaType::CreateFromBinaryName("java.lang.Object");
+ ObjectRefs object_refs;
+ mojom::GinJavaBridgeError error =
+ mojom::GinJavaBridgeError::kGinJavaBridgeNoError;
+
+ mojo::FakeMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ CoerceJavaScriptValueToJavaValue(env, bad_value, target_type, true,
+ object_refs, &error);
+
+ EXPECT_EQ("Malformed GinJavaBridgeValue",
+ bad_message_observer.WaitForBadMessage());
+}
+
} // namespace content
Original Bug Report
Heap buffer overflow in GinJavaBridgeValue deserialization allows compromised renderer to read host app process memory
Heap buffer overflow in GinJavaBridgeValue deserialization allows compromised renderer to read host app process memory
Summary
A compromised renderer process can trigger a heap buffer overflow read in the host application process on Android by sending a malformed BinaryValue through the GinJavaBridgeRemoteObject.InvokeMethod Mojo interface. The argument coercion path in CoerceGinJavaBridgeValueToJavaValue trusts that every BinaryValue is a valid GinJavaBridgeValue, with the only validation being a DCHECK that is compiled out of release builds. When a renderer supplies a BinaryValue shorter than the expected GinJavaBridgeValue::Header, the subsequent call to Pickle::headerT<Header>() reads beyond the allocated buffer. This affects all Android applications that embed a WebView and register any Java bridge object via addJavascriptInterface. The overflow occurs in the unsandboxed host app process, making it a sandbox escape primitive when combined with a renderer compromise.
Platform: Android (all architectures). Requires a WebView embedder that calls addJavascriptInterface.
Bisect
Introducing Commit: 21d14e471385296126beac77a9b9a4bf3fabadd4
- Date: 2014-04-28
- Author: mnaganov@chromium.org
- Subject: [Android] Add GinJavaBridgeValue for Gin Java Bridge
The GinJavaBridgeValue type and its GetType() method have existed since the initial implementation. The DCHECK-only validation in CoerceGinJavaBridgeValueToJavaValue was never upgraded to a runtime check.
Root Cause
When a renderer invokes a Java bridge method through GinJavaBridgeRemoteObject.InvokeMethod, the browser side receives a base::ListValue of arguments. Each argument is coerced to a Java type by CoerceJavaScriptValueToJavaValue. When the argument type is base::Value::Type::BINARY, the code unconditionally dispatches to CoerceGinJavaBridgeValueToJavaValue:
// content/browser/android/java/gin_java_script_to_java_types_coercion.cc:763-765
case base::Value::Type::BINARY:
return CoerceGinJavaBridgeValueToJavaValue(
env, value, target_type, coerce_to_string, object_refs, error);
Inside that function, the sole check that the binary blob is actually a well-formed GinJavaBridgeValue is a DCHECK, which does not exist in release builds:
// content/browser/android/java/gin_java_script_to_java_types_coercion.cc:692-695
DCHECK(GinJavaBridgeValue::ContainsGinJavaBridgeValue(&value));
std::unique_ptr<const GinJavaBridgeValue> gin_value(
GinJavaBridgeValue::FromValue(&value));
switch (gin_value->GetType()) {
FromValue constructs a GinJavaBridgeValue from the raw blob bytes without any size validation. GetType() then interprets the internal pickle’s header as the extended Header struct:
// content/common/android/gin_java_bridge_value.cc:22-27
#pragma pack(push, 4)
struct Header : public base::Pickle::Header {
uint32_t magic;
int32_t type;
};
#pragma pack(pop)
// content/common/android/gin_java_bridge_value.cc:89-92
GinJavaBridgeValue::Type GinJavaBridgeValue::GetType() const {
const Header* header = pickle_.headerT<Header>();
DCHECK(header->type >= TYPE_FIRST_VALUE && header->type < TYPE_LAST_VALUE);
return static_cast<Type>(header->type);
}
The Header struct is 12 bytes (4-byte payload_size from base::Pickle::Header, 4-byte magic, 4-byte type). A compromised renderer can send a BinaryValue containing only 4 bytes, which is sufficient for base::Pickle::WithData to consider the pickle valid (the payload_size field fits), but headerT<Header>() will read 12 bytes from a 4-byte allocation, producing a heap buffer overflow of 8 bytes. The ContainsGinJavaBridgeValue function does perform the correct size check (value->GetBlob().size() < sizeof(Header)), but it is only called inside the DCHECK.
The overflow occurs on the JavaBridgeThread in the host app process, not in the renderer. Since Android WebView host app processes are unsandboxed and hold the full privilege set of the embedding application, this provides a direct information disclosure primitive from the renderer sandbox into the app process heap.
Reproduce
Tested at commit ab3f3f8b586d6f03cf15aaf4ee343bed9e0ed2d1 on Android arm64 (Pixel 7 Pro, HWASAN userdebug build). Two APKs are involved: system_webview_64_apk is the WebView engine (contains libwebviewchromium.so with the renderer patch), and system_webview_shell_apk is a test host app whose WebViewLayoutTestActivity registers a Java bridge (awConsole) via addJavascriptInterface. Two reproduction methods are provided; each is self-contained.
Method A: CDP navigation (no shell modification)
- Apply the renderer-side patch and build:
cd ~/chromium/src
git apply patch.diff
out/android/args.gn:
is_debug = false
dcheck_always_on = false
target_cpu = "arm64"
is_component_build = false
target_os = "android"
is_hwasan = true
android_static_analysis = "off"
incremental_install = false
autoninja -C out/android system_webview_64_apk system_webview_shell_apk
- Install both APKs and set the WebView provider:
out/android/bin/system_webview_64_apk install
out/android/bin/system_webview_shell_apk install
adb shell cmd webviewupdate set-webview-implementation com.android.webview
- Serve the PoC, launch the shell, and navigate via Chrome DevTools Protocol:
python3 -m http.server 8888 &
adb reverse tcp:8888 tcp:8888
adb shell am start -n org.chromium.webview_shell/.WebViewLayoutTestActivity
PID=$(adb shell pidof org.chromium.webview_shell)
adb forward tcp:9222 localabstract:webview_devtools_remote_$PID
python3 navigate.py
- Check crash output:
adb logcat -d | grep -E "(HWAddressSanitizer|heap-buffer|SUMMARY)"
Method B: data URI (no CDP, no HTTP server)
- Apply the renderer-side patch:
cd ~/chromium/src
git apply patch.diff
- Add one line to
android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewLayoutTestActivity.java, at the end ofonCreate, after the existingaddJavascriptInterfacecall:
mWebView.loadUrl("data:text/html,<script>awConsole.log('trigger')</script>");
- Build with the same
out/android/args.gnas Method A:
autoninja -C out/android system_webview_64_apk system_webview_shell_apk
- Install both APKs, set the WebView provider, and launch:
out/android/bin/system_webview_64_apk install
out/android/bin/system_webview_shell_apk install
adb shell cmd webviewupdate set-webview-implementation com.android.webview
adb shell am start -n org.chromium.webview_shell/.WebViewLayoutTestActivity
- Check crash output:
adb logcat -d | grep -E "(HWAddressSanitizer|heap-buffer|SUMMARY)"
The host app process crashes within one second. The overflow occurs in the WebView host app process (PID 3697, org.chromium.webview_shell), not in the renderer (PID 3727, sandboxed child process). The crashing thread is T919 (JavaBridgeThread), the dedicated background thread that processes Java bridge invocations from Mojo IPC. Symbolized HWASAN output from adb logcat:
==3697==ERROR: HWAddressSanitizer: tag-mismatch on address 0x0043d829dc08 at pc 0x007021178a34
READ of size 4 at 0x0043d829dc08 tags: 40/04(40) (ptr/mem) in thread T919
#0 GinJavaBridgeValue::GetType() gin_java_bridge_value.cc:92
#1 CoerceGinJavaBridgeValueToJavaValue() gin_java_script_to_java_types_coercion.cc:695
#2 CoerceJavaScriptValueToJavaValue() gin_java_script_to_java_types_coercion.cc:764
#3 GinJavaMethodInvocationHelper::Invoke() gin_java_method_invocation_helper.cc:137
#4 GinJavaBridgeDispatcherHost::OnInvokeMethod() gin_java_bridge_dispatcher_host.cc:384
#5 GinJavaBridgeDispatcherHost::InvokeMethod() gin_java_bridge_dispatcher_host.cc:488
#6 GinJavaBridgeRemoteObjectStubDispatch::AcceptWithResponder() gin_java_bridge.mojom.cc:1693
#7 mojo::InterfaceEndpointClient::HandleValidatedMessage()
#8 mojo::MessageDispatcher::Accept()
#9 mojo::InterfaceEndpointClient::HandleIncomingMessage()
#10 mojo::internal::MultiplexRouter::ProcessIncomingMessage()
...
[0x0043d829dc00,0x0043d829dc20) is a small allocated heap chunk; size: 32 offset: 8
Cause: heap-buffer-overflow
0x0043d829dc08 is located 4 bytes after a 4-byte region [0x0043d829dc00,0x0043d829dc04)
allocated by thread T919 here:
#0 operator new libclang_rt.hwasan
#1 malloc libc.so
#2 base::Pickle::Resize() pickle.cc:424
#3 base::Pickle::Pickle(base::Pickle const&) pickle.cc:335
#4 GinJavaBridgeValue::GinJavaBridgeValue() gin_java_bridge_value.cc:134
#5 CoerceGinJavaBridgeValueToJavaValue() gin_java_script_to_java_types_coercion.cc:694
SUMMARY: HWAddressSanitizer: tag-mismatch (libwebviewchromium.so+0x59e2a34)
Suggested Fix
Replace the DCHECK in CoerceGinJavaBridgeValueToJavaValue with a runtime validation that rejects malformed blobs before constructing a GinJavaBridgeValue. The ContainsGinJavaBridgeValue helper already implements the correct size, magic, and type-range checks; it simply needs to be called unconditionally:
// content/browser/android/java/gin_java_script_to_java_types_coercion.cc
jvalue CoerceGinJavaBridgeValueToJavaValue(JNIEnv* env,
const base::Value& value,
const JavaType& target_type,
bool coerce_to_string,
const ObjectRefs& object_refs,
mojom::GinJavaBridgeError* error) {
- DCHECK(GinJavaBridgeValue::ContainsGinJavaBridgeValue(&value));
+ if (!GinJavaBridgeValue::ContainsGinJavaBridgeValue(&value)) {
+ *error = mojom::GinJavaBridgeError::kGinJavaBridgeNonAssignableTypes;
+ return jvalue();
+ }
std::unique_ptr<const GinJavaBridgeValue> gin_value(
GinJavaBridgeValue::FromValue(&value));
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.