Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebShare
DescriptionUse after free in WebShare
ComponentWebShare
Bug ClassUAF
Tracker496266444
Fix commit5678c0f8a561 (chromium/src) +133/-51
CISA KEVNot listed
CreditedDavid Sievers
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
modified

Files Changed

  • components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
From 5678c0f8a561cd9f93bd0f58a9352b314160e2cf Mon Sep 17 00:00:00 2001
From: Dibyajyoti Pal <dibyapal@google.com>
Date: Fri, 24 Apr 2026 17:03:40 -0700
Subject: [PATCH] [WebShare] Fix threading violations and race conditions in Android

This commit addresses several critical stability issues identified
in the Android WebShare file transfer implementation:

1. Fixes sequence affinity violations by moving the creation and
   starting of the Mojo Watcher from the background thread pool to
   the Main (UI) thread in ShareServiceImpl by utilizing
   onPostExecute of the AsyncTask.
2. Decouples Mojo events from blocking file I/O in BlobReceiver by
   posting the read operations to the USER_BLOCKING background
   sequence.
3. Prevents use-after-free crashes and heap corruption by adding
   state checks (mIsClosed) at callback entry points in
   BlobReceiver, and reordering destruction in reportError to close
   handles before executing callbacks.

Verified that this no longer causes Android chrome to crash or OOM
with the compromised poc and the renderer changes. Also verified
that "normal" share flow hasn't changed manually.

Fixed: 496266444
Change-Id: I6bcd235826fdfb1ece82bd0b8b25cc30d4a0d4fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7791704
Reviewed-by: Adriana Ixba <aixba@chromium.org>
Commit-Queue: Dibyajyoti Pal <dibyapal@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620567}
---

diff --git a/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java b/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
index 737b0113..b04cee07 100644
--- a/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
+++ b/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
@@ -9,6 +9,9 @@
 import org.chromium.base.Callback;
 import org.chromium.base.Log;
 import org.chromium.base.StreamUtil;
+import org.chromium.base.task.PostTask;
+import org.chromium.base.task.TaskRunner;
+import org.chromium.base.task.TaskTraits;
 import org.chromium.blink.mojom.Blob;
 import org.chromium.blink.mojom.BlobReaderClient;
 import org.chromium.build.annotations.NullMarked;
@@ -26,7 +29,11 @@
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
 
-/** Receives a blob over mojom and writes it to the stream. */
+/**
+ * Receives a blob over mojom and writes it to the stream. Even though it starts working on whatever
+ * thread it is created on, the final callback is run on the UI thread, while file operations work
+ * in the background thread.
+ */
 @NullMarked
 public class BlobReceiver implements BlobReaderClient {
     private static final String TAG = "share";
@@ -38,20 +45,24 @@
     private final long mMaximumContentSize;
     private long mExpectedContentSize;
     private long mReceivedContentSize;
+    private volatile boolean mIsClosed;
     private DataPipe.@Nullable ConsumerHandle mConsumerHandle;
-    private @Nullable Callback<Integer> mCallback;
+    private volatile @Nullable Callback<Integer> mCallback;
     private @Nullable Blob mBlob;
+    private final TaskRunner mTaskRunner;
 
     /**
      * Constructs a BlobReceiver.
      *
      * @param outputStream the destination for the blob contents.
      * @param maximumContentSize the maximum permitted length of the blob.
+     * @param taskRunner the task runner for background execution.
      */
-    public BlobReceiver(OutputStream outputStream, long maximumContentSize) {
+    public BlobReceiver(OutputStream outputStream, long maximumContentSize, TaskRunner taskRunner) {
         mBuffer = ByteBuffer.allocateDirect(CHUNK_SIZE);
         mOutputStream = outputStream;
         mMaximumContentSize = maximumContentSize;
+        mTaskRunner = taskRunner;
     }
 
     /**
@@ -82,38 +93,62 @@
     // ConnectionErrorHandler
     @Override
     public void onConnectionError(MojoException e) {
-        if (mCallback == null) return;
-        reportError(e.getMojoResult(), "Connection error detected.");
+        mTaskRunner.execute(
+                () -> {
+                    if (mIsClosed) return;
+                    assumeNonNull(mCallback);
+                    reportError(e.getMojoResult(), "Connection error detected.");
+                });
     }
 
     // BlobReaderClient
     @Override
     public void onCalculatedSize(long totalSize, long expectedContentSize) {
-        if (mCallback == null) return;
-        if (expectedContentSize > mMaximumContentSize) {
-            reportError(MojoResult.RESOURCE_EXHAUSTED, "Stream exceeds permitted size");
-            return;
-        }
-        mExpectedContentSize = expectedContentSize;
-        if (mReceivedContentSize >= mExpectedContentSize) {
-            complete();
-            return;
-        }
+        mTaskRunner.execute(
+                () -> {
+                    if (mIsClosed) return;
+                    assumeNonNull(mCallback);
+                    if (expectedContentSize > mMaximumContentSize) {
+                        reportError(MojoResult.RESOURCE_EXHAUSTED, "Stream exceeds permitted size");
+                        return;
+                    }
+                    mExpectedContentSize = expectedContentSize;
+                    if (mReceivedContentSize >= mExpectedContentSize) {
+                        complete();
+                        return;
+                    }
+                    final DataPipe.ConsumerHandle handle = mConsumerHandle;
+                    assumeNonNull(handle);
+                    PostTask.postTask(
+                            TaskTraits.UI_DEFAULT,
+                            () -> {
+                                startWatcher(handle);
+                            });
+                });
+    }
 
+    private void startWatcher(final DataPipe.ConsumerHandle consumerHandle) {
+        if (mIsClosed) return;
+        assumeNonNull(mCallback);
         Watcher watcher = CoreImpl.getInstance().getWatcher();
-        assumeNonNull(mConsumerHandle);
+        assumeNonNull(consumerHandle);
         watcher.start(
-                mConsumerHandle,
+                consumerHandle,
                 Core.HandleSignals.READABLE,
                 new Watcher.Callback() {
                     @Override
                     public void onResult(int result) {
-                        if (mCallback == null) return;
-                        if (result == MojoResult.OK) {
-                            read();
-                        } else {
-                            reportError(result, "Watcher reported error.");
-                        }
+                        mTaskRunner.execute(
+                                () -> {
+                                    if (mIsClosed) {
+                                        return;
+                                    }
+                                    if (result == MojoResult.OK) {
+                                        read();
+                                    } else {
+                                        reportError(result, "Watcher reported error.");
+                                    }
+                                });
                     }
                 });
     }
@@ -121,8 +156,12 @@
     // BlobReaderClient
     @Override
     public void onComplete(int status, long dataLength) {
-        if (mCallback == null) return;
-        read();
+        mTaskRunner.execute(
+                () -> {
+                    if (mIsClosed) return;
+                    assumeNonNull(mCallback);
+                    read();
+                });
     }
 
     private void read() {
@@ -166,6 +205,8 @@
         }
     }
 
+    // All functions below are called only from the task runner thread, with the callback and the
+    // blob passed to the UI thread for running.
     private void complete() {
         try {
             mOutputStream.close();
@@ -186,19 +227,32 @@
     }
 
     private void invokeCallbackAndCloseMojoEndpoints(int result) {
-        if (mBlob != null) {
Loading diff…

Original Bug Report

reported by da...@gmail.com

Mojo handle double-close / use after close leads to uaf/double free

Steps to reproduce the problem

gn args:

android_static_analysis = "off"
debuggable_apks = true
enable_android_secondary_abi = true
dcheck_always_on=false
is_debug=false
ffmpeg_branding = "Chrome"
is_component_build = false
proprietary_codecs = true
symbol_level = 2
target_cpu = "arm64"
target_os = "android"
use_reclient = false
use_siso = true
use_full_mte=true

apply patch.diff enable mte on device

python3 -m http.server
adb reverse tcp:8000 tcp:8000

open http://localhost:8000/poc.html

observe crash on adb logcat

The uaf/double-free triggers around 50% of the time on my device tested on commit 1fa21d15780b5f89014de6a7a27ada0b49fb4693

Problem Description

With ipcz mojo handle ids are are normal pointers, meaning that any code that incorrectly closes mojo handles now triggers a use-after-free / double free.

In the java code for ShareServiceImpl some handles are handed to a background thread for processing. These handles can be closed by the renderer so that by the time the background thread processes them either the backing objects are used after being freed or freed again.

Summary

Mojo handle double-close / use after close leads to uaf/double free

Custom Questions

Type of crash:

browser crash

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker