Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CameraCapture
DescriptionUse after free in CameraCapture
ComponentCameraCapture
Bug ClassUAF
Tracker516987782
Fix commitb2c70fcd2ebf (chromium/src) +66/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-16

Changed Functions

FunctionChangeNotes
if
media/capture/video/apple/video_capture_device_apple.mm
modified
TEST
media/capture/video/apple/video_capture_device_apple_unittest.mm
modified
for
media/capture/video/apple/video_capture_device_apple_unittest.mm
modified
MockImageCaptureClient
media/capture/video/apple/video_capture_device_apple_unittest.mm
modified

Files Changed

  • media/capture/video/apple/video_capture_device_apple.h
  • media/capture/video/apple/video_capture_device_apple.mm
  • media/capture/video/apple/video_capture_device_apple_unittest.mm
From b2c70fcd2ebf3fef59465251277e6105c9f5f5a3 Mon Sep 17 00:00:00 2001
From: Sangwhan Moon <[email protected]>
Date: Sat, 04 Jul 2026 00:47:03 -0700
Subject: [PATCH] [mac] Fix use-after-free in VideoCaptureDeviceApple Mojo callback

AVFoundation invokes OnPhotoTaken and OnPhotoError on an arbitrary
background queue. VideoCaptureDeviceApple::OnPhotoTaken was previously
executing std::move(photo_callback_).Run(...) directly on that
background thread, which causes threading violations and potential
use-after-free/double-free issues if the VideoCaptureDeviceApple
instance is destroyed concurrently, or if photo_callback_ is accessed
while being modified on the main task runner.

Bug: 516987782
Change-Id: Ibefa689f22116a4e5e3d89da7a3476af36c55251
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7912588
Reviewed-by: Ilya Nikolaevskiy <[email protected]>
Commit-Queue: Sangwhan Moon <[email protected]>
Auto-Submit: Sangwhan Moon <[email protected]>
Reviewed-by: Ted (Chromium) Meyer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1656796}
---

diff --git a/media/capture/video/apple/video_capture_device_apple.h b/media/capture/video/apple/video_capture_device_apple.h
index 8b23902f4..0f0c210 100644
--- a/media/capture/video/apple/video_capture_device_apple.h
+++ b/media/capture/video/apple/video_capture_device_apple.h
@@ -114,6 +114,8 @@
 
   VideoFrameMetadata GetVideoFrameMetadata();
 
+  void OnPhotoResultOnMainThread(mojom::BlobPtr blob);
+
   // Flag indicating the internal state.
   enum InternalState { kNotInitialized, kIdle, kCapturing, kError };
 
diff --git a/media/capture/video/apple/video_capture_device_apple.mm b/media/capture/video/apple/video_capture_device_apple.mm
index b7899c4..352803d 100644
--- a/media/capture/video/apple/video_capture_device_apple.mm
+++ b/media/capture/video/apple/video_capture_device_apple.mm
@@ -264,21 +264,43 @@
 void VideoCaptureDeviceApple::OnPhotoTaken(const uint8_t* image_data,
                                            size_t image_length,
                                            const std::string& mime_type) {
-  DCHECK(photo_callback_);
-  if (!image_data || !image_length) {
-    OnPhotoError();
-    return;
+  // Note: While OnPhotoTaken() is called on an AVFoundation background queue by
+  // VideoCaptureDeviceAVFoundation, it is guaranteed that `this` is not deleted
+  // because VideoCaptureDeviceAVFoundation holds `_lock` while calling
+  // `_frameReceiver->OnPhotoTaken(...)`, and StopAndDeAllocate() acquires
+  // `_lock` when setting `_frameReceiver = nil` before destruction on the main
+  // thread. Therefore, accessing `task_runner_` here is safe.
+  mojom::BlobPtr blob;
+  if (image_data && image_length) {
+    blob = mojom::Blob::New();
+    blob->data.assign(image_data, UNSAFE_TODO(image_data + image_length));
+    blob->mime_type = mime_type;
   }
-
-  mojom::BlobPtr blob = mojom::Blob::New();
-  blob->data.assign(image_data, UNSAFE_TODO(image_data + image_length));
-  blob->mime_type = mime_type;
-  std::move(photo_callback_).Run(std::move(blob));
+  task_runner_->PostTask(
+      FROM_HERE,
+      base::BindOnce(&VideoCaptureDeviceApple::OnPhotoResultOnMainThread,
+                     weak_factory_.GetWeakPtr(), std::move(blob)));
 }
 
 void VideoCaptureDeviceApple::OnPhotoError() {
   VLOG(1) << __func__ << " error taking picture";
-  photo_callback_.Reset();
+  task_runner_->PostTask(
+      FROM_HERE,
+      base::BindOnce(&VideoCaptureDeviceApple::OnPhotoResultOnMainThread,
+                     weak_factory_.GetWeakPtr(), nullptr));
+}
+
+void VideoCaptureDeviceApple::OnPhotoResultOnMainThread(mojom::BlobPtr blob) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+  if (!photo_callback_) {
+    return;
+  }
+  if (!blob) {
+    VLOG(1) << __func__ << " error taking picture";
+    photo_callback_.Reset();
+    return;
+  }
+  std::move(photo_callback_).Run(std::move(blob));
 }
 
 void VideoCaptureDeviceApple::ReceiveError(VideoCaptureError error,
diff --git a/media/capture/video/apple/video_capture_device_apple_unittest.mm b/media/capture/video/apple/video_capture_device_apple_unittest.mm
index 59554e2..0302c7e6 100644
--- a/media/capture/video/apple/video_capture_device_apple_unittest.mm
+++ b/media/capture/video/apple/video_capture_device_apple_unittest.mm
@@ -9,8 +9,10 @@
 #import "base/memory/ref_counted.h"
 #import "base/memory/scoped_refptr.h"
 #import "base/run_loop.h"
+#include "base/synchronization/waitable_event.h"
 #include "base/test/bind.h"
 #include "base/test/gmock_callback_support.h"
+#include "base/threading/thread.h"
 #include "media/capture/video/apple/test/fake_av_capture_device_format.h"
 #import "media/capture/video/apple/test/video_capture_test_utils.h"
 #include "media/capture/video/apple/video_capture_device_avfoundation.h"
@@ -136,6 +138,36 @@
   EXPECT_EQ(result, fmt_640_480_2vuy_30);
 }
 
+// OnPhotoTaken() and OnPhotoError() are documented as safe to call from any
+// thread. Exercise OnPhotoError() concurrently from the device task runner and
+// a background thread to ensure the in-flight TakePhoto callback is accessed
+// safely without data races or sequence checker violations.
+TEST(VideoCaptureDeviceMacTest, ConcurrentOnPhotoErrorIsThreadSafe) {
+  RunTestCase(base::BindOnce([] {
+    constexpr int kIterations = 1000;
+    VideoCaptureDeviceDescriptor descriptor;
+    auto device = std::make_unique<VideoCaptureDeviceApple>(descriptor);
+    VideoCaptureDeviceAVFoundationFrameReceiver* frame_receiver = device.get();
+
+    base::Thread other_thread("OnPhotoErrorTestThread");
+    ASSERT_TRUE(other_thread.Start());
+
+    base::WaitableEvent done;
+    other_thread.task_runner()->PostTask(
+        FROM_HERE, base::BindLambdaForTesting([&] {
+          for (int i = 0; i < kIterations; ++i) {
+            frame_receiver->OnPhotoError();
+          }
+          done.Signal();
+        }));
+    for (int i = 0; i < kIterations; ++i) {
+      frame_receiver->OnPhotoError();
+    }
+    done.Wait();
+    other_thread.Stop();
+  }));
+}
+
 class MockImageCaptureClient
     : public base::RefCountedThreadSafe<MockImageCaptureClient> {
  public:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/capture/video/apple/video_capture_device_apple_unittest.mm b/media/capture/video/apple/video_capture_device_apple_unittest.mm
index 59554e2..0302c7e6 100644
--- a/media/capture/video/apple/video_capture_device_apple_unittest.mm
+++ b/media/capture/video/apple/video_capture_device_apple_unittest.mm
@@ -9,8 +9,10 @@
 #import "base/memory/ref_counted.h"
 #import "base/memory/scoped_refptr.h"
 #import "base/run_loop.h"
+#include "base/synchronization/waitable_event.h"
 #include "base/test/bind.h"
 #include "base/test/gmock_callback_support.h"
+#include "base/threading/thread.h"
 #include "media/capture/video/apple/test/fake_av_capture_device_format.h"
 #import "media/capture/video/apple/test/video_capture_test_utils.h"
 #include "media/capture/video/apple/video_capture_device_avfoundation.h"
@@ -136,6 +138,36 @@
   EXPECT_EQ(result, fmt_640_480_2vuy_30);
 }
 
+// OnPhotoTaken() and OnPhotoError() are documented as safe to call from any
+// thread. Exercise OnPhotoError() concurrently from the device task runner and
+// a background thread to ensure the in-flight TakePhoto callback is accessed
+// safely without data races or sequence checker violations.
+TEST(VideoCaptureDeviceMacTest, ConcurrentOnPhotoErrorIsThreadSafe) {
+  RunTestCase(base::BindOnce([] {
+    constexpr int kIterations = 1000;
+    VideoCaptureDeviceDescriptor descriptor;
+    auto device = std::make_unique<VideoCaptureDeviceApple>(descriptor);
+    VideoCaptureDeviceAVFoundationFrameReceiver* frame_receiver = device.get();
+
+    base::Thread other_thread("OnPhotoErrorTestThread");
+    ASSERT_TRUE(other_thread.Start());
+
+    base::WaitableEvent done;
+    other_thread.task_runner()->PostTask(
+        FROM_HERE, base::BindLambdaForTesting([&] {
+          for (int i = 0; i < kIterations; ++i) {
+            frame_receiver->OnPhotoError();
+          }
+          done.Signal();
+        }));
+    for (int i = 0; i < kIterations; ++i) {
+      frame_receiver->OnPhotoError();
+    }
+    done.Wait();
+    other_thread.Stop();
+  }));
+}
+
 class MockImageCaptureClient
     : public base::RefCountedThreadSafe<MockImageCaptureClient> {
  public:
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free/Double-Free in VideoCaptureDeviceApple due to Unsynchronized Callback

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An unsynchronized cross-thread data race exists in VideoCaptureDeviceApple on macOS and iOS, where photo_callback_ is accessed concurrently without mutual exclusion. The device thread reads and writes the callback in TakePhoto() while an AVFoundation background thread concurrently runs or resets it in OnPhotoTaken() or OnPhotoError(). This race can potentially lead to a Use-After-Free (UAF) or Double Free of the callback’s underlying BindState.

Affected files:

  • media/capture/video/apple/video_capture_device_apple.mm
  • media/capture/video/apple/video_capture_device_apple.h
  • media/capture/video/apple/video_capture_device_avfoundation.mm

Estimated timestamp from git blame: 2016-07-15

Summary

An unsynchronized cross-thread data race in VideoCaptureDeviceApple on macOS and iOS can potentially lead to a Use-After-Free (UAF) and Double Free of a callback’s underlying BindState. This vulnerability is reachable by a compromised renderer or a site with camera permissions spamming Mojo IPCs for image capture.

Root Cause Analysis

In VideoCaptureDeviceApple, the in-flight image capture callback is stored in the member variable photo_callback_, which is a base::OnceCallback<void(mojom::BlobPtr)> (aliased as TakePhotoCallback):

  • File Reference: media/capture/video/apple/video_capture_device_apple.h, line 132:
    TakePhotoCallback photo_callback_;
    

This member is accessed and modified across different threads without any lock protection or synchronization:

  1. Device Thread (task_runner_): In VideoCaptureDeviceApple::TakePhoto, the callback is read and assigned on the device thread:

    • File Reference: media/capture/video/apple/video_capture_device_apple.mm, lines 129–139:
      void VideoCaptureDeviceApple::TakePhoto(TakePhotoCallback callback) {
        DCHECK(task_runner_->BelongsToCurrentThread());
        DCHECK(state_ == kCapturing) << state_;
      
        if (photo_callback_) {
          return;
        }
        photo_callback_ = std::move(callback);
        [capture_device_ takePhoto];
      }
      
  2. AVFoundation Delegate Thread: When a photo is successfully processed or encounters an error, the AVFoundation delegate thread (an arbitrary background thread / dispatch queue) directly invokes OnPhotoTaken() or OnPhotoError() on the receiver. Unlike other receiver methods like ReceiveError which use task_runner_->PostTask to safely hop back to the device thread, these methods execute directly on the delegate’s thread:

    • File Reference: media/capture/video/apple/video_capture_device_apple.mm, lines 264–282:
      void VideoCaptureDeviceApple::OnPhotoTaken(const uint8_t* image_data,
                                                 size_t image_length,
                                                 const std::string& mime_type) {
        DCHECK(photo_callback_);
        ...
        std::move(photo_callback_).Run(std::move(blob));
      }
      
      void VideoCaptureDeviceApple::OnPhotoError() {
        VLOG(1) << __func__ << " error taking picture";
        photo_callback_.Reset();
      }
      

Potential Concurrency / Exploitation Mechanism

Because OnceCallback relies on a non-atomic reference-counted BindState object (scoped_refptr<BindStateBase>), concurrent non-atomic accesses on two threads can lead to memory corruption:

  1. Thread B (AVFoundation background thread) executes OnPhotoTaken() and performs std::move(photo_callback_).Run(...). During the move, it reads the address of the underlying BindState (BindState_A) into a CPU register.
  2. Thread A (Device thread) concurrently processes a new TakePhoto() request. It reads photo_callback_ before Thread B has written nullptr to clear it. Thread A then executes photo_callback_ = std::move(callback), which overwrites the slot with a new callback and calls Release() on the old BindState_A.
  3. Since the reference count of BindState_A drops to 0, the memory allocated for BindState_A is freed.
  4. Thread B completes its move operation and writes nullptr to photo_callback_ (corrupting Thread A’s newly assigned callback), and then invokes the callback using the pointer in its register, which now points to the freed BindState_A object. This results in a Use-After-Free (UAF).

Potential Trigger Steps

Note: These are potential steps as our tooling does not have the ability to run code or verify a live exploit.

  1. A web page with camera permissions (or a compromised Renderer process) initiates connection to the media.mojom.ImageCapture Mojo interface.
  2. The Renderer spams the media.mojom.ImageCapture.TakePhoto Mojo IPC in a tight loop without waiting for the returned promises to resolve.
  3. If timed correctly, the concurrent execution of a new TakePhoto call on the Device Thread and the completion callback on the AVFoundation thread results in the data race, causing a heap corruption crash or potential control flow hijacking.

Impact

  • On macOS, the Video Capture Service runs as kNoSandbox (unsandboxed utility process).
  • On iOS, the service runs inside the browser process.
  • Exploiting this UAF could potentially allow an attacker to escape the renderer sandbox and execute arbitrary code with full user or browser privileges.

Suggested Fix

Ensure that all callback access on the VideoCaptureDeviceApple instance occurs exclusively on the designated task_runner_ thread. The methods OnPhotoTaken and OnPhotoError must marshal execution back to the device thread using task_runner_->PostTask, rather than executing the callback directly on the AVFoundation background thread. For example:

void VideoCaptureDeviceApple::OnPhotoTaken(const uint8_t* image_data,
                                           size_t image_length,
                                           const std::string& mime_type) {
  // Create a copy of the image data to safely pass across threads
  std::vector<uint8_t> data(image_data, image_data + image_length);
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&VideoCaptureDeviceApple::OnPhotoTakenOnDeviceThread,
                     weak_factory_.GetWeakPtr(), std::move(data), mime_type));
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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