Critical watch CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CameraCapture
DescriptionUse after free in CameraCapture
ComponentCameraCapture
Sub-componentMac
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516987782
Fix commitb2c70fcd2ebf (chromium/src) +66/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-04 (public git)
Disclosed2026-07-16
Patch gap12 days — fix public in source before the advisory

Background

`VideoCaptureDeviceApple`
The macOS/iOS media::VideoCaptureDevice implementation that owns the in-flight TakePhoto completion callback (photo_callback_) and a task_runner_ for the sequence it was created on.
`VideoCaptureDeviceAVFoundationFrameReceiver`
The interface (implemented by VideoCaptureDeviceApple) that AVFoundation calls back into via OnPhotoTaken() / OnPhotoError() from an arbitrary background queue when a still photo completes or fails.
`photo_callback_`
A single-shot Mojo TakePhoto reply callback that is std::move-run to deliver the encoded image Blob and then must not be touched again.
`weak_factory_` / `WeakPtr`
A base primitive that lets a posted task be silently dropped if this has already been destroyed, valid only when checked on the owning sequence.

Root Cause Analysis

Prior to the patch, VideoCaptureDeviceApple::OnPhotoTaken ran std::move(photo_callback_).Run(std::move(blob)) and VideoCaptureDeviceApple::OnPhotoError ran photo_callback_.Reset() directly on whichever AVFoundation background queue invoked them, while the same photo_callback_ member is otherwise owned and mutated on the device’s task_runner_ sequence. Because these callbacks execute off-sequence, a concurrent StopAndDeAllocate()/destruction on the main thread — or concurrent access to photo_callback_ while it is being modified — can free or reassign the callback state underneath the background thread, producing the use-after-free / double-free described in the advisory (running or resetting a callback whose backing state is being torn down). The removed DCHECK(photo_callback_) also assumed the callback was live and untouched by another thread, an invariant that off-sequence execution does not guarantee.

The fix keeps all photo_callback_ access on task_runner_: both entry points now only build the mojom::BlobPtr and PostTask to the new OnPhotoResultOnMainThread, bound through weak_factory_.GetWeakPtr() so the task is dropped if the device is gone. OnPhotoResultOnMainThread asserts task_runner_->BelongsToCurrentThread(), null-checks photo_callback_ before use, and only then resets it (error/nullptr blob) or runs it, so the callback is always accessed on its owning sequence and never after destruction.

Key insight
The single core mistake is touching sequence-affine state (photo_callback_, and implicitly this) directly on an AVFoundation background queue instead of the owning task_runner_; the fix marshals the result back to that sequence via a weak-pointer-bound PostTask to OnPhotoResultOnMainThread, which re-checks liveness before any access.

Attack Path

  1. Initiate a still capture Web content with granted camera access issues an ImageCapture/TakePhoto request, arming photo_callback_ on the device’s task_runner_ and starting an AVFoundation photo operation.
  2. Off-sequence completion callback AVFoundation completes (or errors) and invokes VideoCaptureDeviceApple::OnPhotoTaken/OnPhotoError on an arbitrary background queue, where the pre-patch code ran std::move(photo_callback_).Run(...) / photo_callback_.Reset() with no synchronization to the owning sequence.
  3. Concurrent teardown race Roughly simultaneously the device is stopped/destroyed on the main thread (StopAndDeAllocate() per the added comment), so the background thread accesses photo_callback_ (and this) while it is being modified or freed — the use-after-free/double-free window, which the added ConcurrentOnPhotoErrorIsThreadSafe test reproduces by hammering OnPhotoError() from two threads.
  4. Memory corruption Running or resetting a callback whose backing state has been reclaimed corrupts the freed callback/device state; controlling the timing and the freed-then-reallocated memory could allow this to be turned into a stronger primitive (inferred).
  5. Downstream escalation Leveraging the corruption toward code execution in the capture/GPU or browser process would be a further step the diff does not demonstrate (inferred).

Impact Assessment

An attacker with an active camera-capture session on macOS/iOS can race the AVFoundation photo callback against device teardown to trigger a use-after-free / double-free of photo_callback_ and the VideoCaptureDeviceApple instance, corrupting process memory in the context that hosts video capture. Preconditions are a granted camera stream and the ability to induce the completion/error callback while stopping capture, and successful exploitation is inherently timing-dependent. Any progression beyond memory corruption to a read/write primitive or code execution is (inferred); the advisory reports this as a use-after-free in CameraCapture and does not state exploited-in-the-wild status here.

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

Audit Directions

  • Off-sequence callback delivery
    Audit every OS/framework callback that runs on a foreign thread or queue (AVFoundation, platform camera/audio, hardware event handlers) for direct access to sequence-affine members like reply callbacks; require marshaling via PostTask with a WeakPtr instead.
  • Single-shot Mojo reply callbacks under teardown
    Look for std::move(callback_).Run() / callback_.Reset() invoked without a same-sequence guard, especially where a Stop/DeAllocate/destructor can run concurrently, and confirm liveness is re-checked on the owning thread before use.
  • Locks vs. sequence affinity mismatch
    Where an object relies on a lock (_lock) to keep this alive across a cross-thread call but its members are otherwise sequence-bound, verify the members touched inside that call are actually thread-safe, not merely protected against destruction.
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