High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ImageCapture
DescriptionUse after free in ImageCapture
ComponentImageCapture
Sub-componentBlink
Bug ClassUAF
Tracker574027204
Fix commitadb8ef84f8fe (chromium/src) +52/-6
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

Background

ImageCapture
A Blink Oilpan GarbageCollected object (third_party/blink/renderer/modules/imagecapture/image_capture.{h,cc}) exposing the ImageCapture web API. It owns the helper that actually grabs frames.
ImageCaptureFrameGrabber
A non-Oilpan helper (subclasses MediaStreamVideoSink) owned by ImageCapture. It is created/used/destroyed on the main thread and relies on ~MediaStreamVideoSink() to disconnect from the track. Before the patch it was held by std::unique_ptr in ImageCapture::frame_grabber_.
SingleShotFrameHandler
Inner handler whose OnVideoFrameOnIOThread() runs on the IO thread; it PostCrossThreadTasks ImageCaptureFrameGrabber::OnVideoFrame() to the main-thread task runner, bound with a base::WeakPtr<ImageCaptureFrameGrabber>.
base::WeakPtr
Validates the pointer only at callback entry; it does not keep the pointee alive while the callback is executing on the stack.
EstablishGpuChannelSync()
Called transitively from OnVideoFrame() via GetRasterContextProvider(); it can synchronously run queued GPU-channel callbacks inline on the main thread, which can allocate on the Oilpan heap and drive lazy sweeping.

Root Cause Analysis

ImageCapture::grabFrame() starts a frame grab without any Persistent<ImageCapture> keeping the Oilpan owner reachable, so if JavaScript drops its reference to the ImageCapture wrapper, ImageCapture becomes garbage-collectable even while an OnVideoFrame() task is pending/executing. OnVideoFrame() was posted bound to a base::WeakPtr<ImageCaptureFrameGrabber>, which only checks liveness at entry. The lifetime invariant violated is that frame_grabber_ (owned via unique_ptr by ImageCapture) must outlive the running OnVideoFrame() call. Inside OnVideoFrame() (image_capture_frame_grabber.cc), GetRasterContextProvider() → SharedGpuContext::CreateContextProviderIfNeeded() → viz::Gpu::EstablishGpuChannelSync() can synchronously flush previously queued EstablishGpuChannel callbacks (e.g. from a prior navigator.gpu.requestAdapter). Such a callback resolves/rejects a promise, allocates on the Oilpan heap, and triggers synchronous lazy sweeping, which runs ~ImageCapture() inline — deleting the unique_ptr-owned frame_grabber_ while OnVideoFrame() is still on the stack. When GetRasterContextProvider() returns, this is dangling: a same-thread, re-entrant use-after-free.

The patch converts ImageCaptureFrameGrabber into a RefCounted<ImageCaptureFrameGrabber> (adding the base class, a friend class RefCounted<>, and moving ~ImageCaptureFrameGrabber() to private), changes ImageCapture::frame_grabber_ from std::unique_ptr to scoped_refptr, allocates it with base::MakeRefCounted, and — crucially — adds scoped_refptr<ImageCaptureFrameGrabber> keep_alive(this); at the top of OnVideoFrame(). That stack reference pins the grabber alive across the GPU-context call even if ~ImageCapture() runs mid-method and drops frame_grabber_. The commit explains why the simpler fixes do not work: WrapPersistent cannot be applied to a non-Oilpan class, and routing through ImageCapture with a CrossThreadHandle would break cancellation on ContextDestroyed(), which depends on resetting frame_grabber_ and invalidating its WeakPtr.

Key insight
A WeakPtr-bound main-thread callback (OnVideoFrame()) made a nested call that could synchronously garbage-collect its own non-Oilpan owner, deleting this mid-execution — because WeakPtr guarantees liveness only at entry, not for the duration of the call.

Attack Path

  1. Prime GPU callback Script issues a navigator.gpu.requestAdapter() (or similar) so an EstablishGpuChannel callback is queued, ready to run inline later.
  2. Start frame grab Script calls ImageCapture.grabFrame() on a live track; GrabFrame() connects a SingleShotFrameHandler to the media stream track. No Persistent<ImageCapture> is held.
  3. Drop the owner Script drops all references to the ImageCapture wrapper so ImageCapture is unreachable in Oilpan (but not yet swept).
  4. Frame delivery A video frame arrives on the IO thread; OnVideoFrameOnIOThread() posts OnVideoFrame() to the main thread. WeakPtr is still valid at entry.
  5. Synchronous GC mid-call OnVideoFrame() → GetRasterContextProvider() → EstablishGpuChannelSync() flushes the queued GPU callback, which resolves a promise, allocates on the Oilpan heap, and triggers lazy sweeping → ~ImageCapture() frees frame_grabber_.
  6. Primitive Control returns into OnVideoFrame() with this pointing at freed memory: a controllable same-thread UAF in the renderer (further escalation to RCE inferred).

Impact Assessment

Renderer-process use-after-free, reachable from untrusted web content via the ImageCapture.grabFrame() API combined with WebGPU adapter requests to seed a synchronously-flushable GPU-channel callback. No elevated privilege is required beyond camera/track access typical for the ImageCapture API. The diff substantiates a precise same-thread re-entrant UAF of the ImageCaptureFrameGrabber while OnVideoFrame() executes; the strength is a genuine UAF (not merely a crash), and heap grooming of the freed object could yield memory corruption and potential code execution (inferred). The advisory’s “Use after free in ImageCapture” matches the diff exactly.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/imagecapture/image_capture.cc
modified
SkImage
third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
modified
ImageCaptureFrameGrabber
third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
modified
ScopedPromiseResolver
third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
modified
SingleShotFrameHandler
third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
modified
TEST_F
third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
modified
if
third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
  • third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
  • third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc
  • third_party/blink/renderer/modules/imagecapture/image_capture.cc
  • third_party/blink/renderer/modules/imagecapture/image_capture.h

Audit Directions

  • WeakPtr-bound cross-thread callbacks into GPU/context setup
    Look for other main-thread callbacks bound by base::WeakPtr that call GetRasterContextProvider() / SharedGpuContext / EstablishGpuChannelSync(); any that can synchronously run promise resolution risk re-entrant self-destruction.
  • Non-Oilpan helpers owned by GarbageCollected objects
    Audit other Blink modules where a GarbageCollected object owns a non-Oilpan helper by unique_ptr and posts tasks referencing the helper; WeakPtr entry-only checks do not protect against sweeping the owner mid-call.
  • Synchronous lazy sweeping triggers
    Enumerate code paths that allocate on the Oilpan heap while another object is on the stack and unreachable, especially where GPU-channel callbacks resolve promises inline.
  • ContextDestroyed cancellation assumptions
    Verify any refactor to CrossThreadHandle/HeapMojoRemote routing preserves the ContextDestroyed()-based cancellation this class relies on.
From adb8ef84f8fe2ae3fe8b3495d3e84626d17acf8a Mon Sep 17 00:00:00 2001
From: Colin Blundell <blundell@chromium.org>
Date: Mon, 19 Oct 2026 13:21:25 -0700
Subject: [PATCH] blink: Fix ImageCaptureFrameGrabber UAF in OnVideoFrame

ImageCapture is an Oilpan GarbageCollected object that owns a
non-Oilpan ImageCaptureFrameGrabber helper [1], which is constructed,
used, and destroyed exclusively on the main thread. In grabFrame() [2],
ImageCaptureFrameGrabber::GrabFrame() connects a SingleShotFrameHandler
to the media stream track [3]. When a frame arrives on the IO thread,
SingleShotFrameHandler::OnVideoFrameOnIOThread() posts
ImageCaptureFrameGrabber::OnVideoFrame() back to the main thread task
runner bound with a WeakPtr<ImageCaptureFrameGrabber> [4] (so the
WeakPtr is only ever dereferenced and invalidated on the main thread).
Because nothing holds a Persistent<ImageCapture> while grabFrame() is
pending, ImageCapture becomes unreachable in Oilpan if JavaScript drops
its reference to the ImageCapture wrapper.

This leads to a same-thread re-entrant UAF on the main thread:
1. ImageCaptureFrameGrabber::OnVideoFrame() starts on the main thread
   [5] before Oilpan has swept the dead ImageCapture. base::WeakPtr
   only checks validity at callback entry; it does not keep
   ImageCaptureFrameGrabber alive while OnVideoFrame() is on the stack.
2. OnVideoFrame() calls GetRasterContextProvider() [5] ->
   SharedGpuContext::ContextProviderWrapper() ->
   SharedGpuContext::CreateContextProviderIfNeeded() [6] ->
   RendererBlinkPlatformImpl::
   CreateSharedOffscreenGraphicsContext3DProvider() ->
   viz::Gpu::EstablishGpuChannelSync() [7] on the main thread.
3. EstablishGpuChannelSync() calls EstablishRequest::FinishOnMain() ->
   Gpu::OnEstablishedGpuChannel() -> Gpu::RunEstablishCallbacks() [8],
   which synchronously runs any queued EstablishGpuChannel callbacks
   inline on the main thread before returning.
4. A queued callback (e.g. from an earlier navigator.gpu.requestAdapter
   call [9]) runs inline on the main thread and resolves/rejects its
   promise, which allocates on the Oilpan heap and triggers Oilpan lazy
   sweeping synchronously on the main thread.
5. Lazy sweeping runs ~ImageCapture() inline on the main thread,
   deleting frame_grabber_ while OnVideoFrame() is still on the stack.
   When GetRasterContextProvider() returns to OnVideoFrame(), "this" is
   dangling [5].

We cannot simply bind OnVideoFrame() with WrapPersistent like
getPhotoCapabilities, getPhotoSettings, and takePhoto do:
- ImageCaptureFrameGrabber is not an Oilpan GarbageCollected class (it
  subclasses MediaStreamVideoSink and relies on ~MediaStreamVideoSink()
  to disconnect from the track), so WrapPersistent cannot be used on it.
- Routing OnVideoFrame() through ImageCapture with a CrossThreadHandle
  would break cancellation on ContextDestroyed(): whereas
  HeapMojoRemote automatically drops pending callbacks on
  ContextDestroyed(), OnVideoFrame() is posted from the IO thread via
  PostCrossThreadTask to a raw SingleThreadTaskRunner and relies on
  ImageCapture::ContextDestroyed() resetting frame_grabber_ (and
  invalidating its WeakPtr) to cancel any pending or already-posted
  frame grab.

Instead, make ImageCaptureFrameGrabber RefCounted (single-threaded on
the main thread, shared only between ImageCapture and an actively
executing OnVideoFrame() call) and hold a local
scoped_refptr<ImageCaptureFrameGrabber> keep_alive on the stack across
OnVideoFrame().

Note: The bug report's suggested patch also included a test for
ImageCapture::ContextDestroyed() running synchronously during
OnVideoFrame(), but auditing the EstablishGpuChannel() callers shows
that none of them can synchronously run JavaScript or detach a frame
(promise resolution uses V8DoNotRunMicrotasksScope), so synchronous
execution context destruction during OnVideoFrame() is not possible.

[1] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/imagecapture/image_capture.h;l=180
[2] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/imagecapture/image_capture.cc;l=1689-1692
[3] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc;l=156-165
[4] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc;l=111-116
[5] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc;l=167-208
[6] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.cc;l=154
[7] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:services/viz/public/cpp/gpu/gpu.cc;l=392-394
[8] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:services/viz/public/cpp/gpu/gpu.cc;l=462-488
[9] https://source.chromium.org/chromium/chromium/src/+/cbf97aa00afa4f6dedcababb7a71efd999986e4b:third_party/blink/renderer/modules/webgpu/gpu.cc;l=299-302

Patch and regression test authored by OpenAI Codex Security (kylebot).

Bug: 574027204
Change-Id: Ib937010017d6601e64d38838f62f14b3aa1b4497
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8588402
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Colin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1722274}
---
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture.cc b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
index 4d35b56..d193a30 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
@@ -1677,7 +1677,7 @@
 
   // Create |m_frameGrabber| the first time.
   if (!frame_grabber_) {
-    frame_grabber_ = std::make_unique<ImageCaptureFrameGrabber>();
+    frame_grabber_ = base::MakeRefCounted<ImageCaptureFrameGrabber>();
   }
 
   if (!frame_grabber_) {
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture.h b/third_party/blink/renderer/modules/imagecapture/image_capture.h
index e477820e..2ce25e4 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture.h
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture.h
@@ -5,9 +5,9 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_IMAGECAPTURE_IMAGE_CAPTURE_H_
 #define THIRD_PARTY_BLINK_RENDERER_MODULES_IMAGECAPTURE_IMAGE_CAPTURE_H_
 
-#include <memory>
 #include <optional>
 
+#include "base/memory/scoped_refptr.h"
 #include "base/time/time.h"
 #include "media/capture/mojom/image_capture.mojom-blink.h"
 #include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
@@ -177,7 +177,7 @@
       MediaTrackConstraintSetType) const;
 
   Member<MediaStreamTrack> stream_track_;
-  std::unique_ptr<ImageCaptureFrameGrabber> frame_grabber_;
+  scoped_refptr<ImageCaptureFrameGrabber> frame_grabber_;
   HeapMojoRemote<media::mojom::blink::ImageCapture> service_;
 
   // Whether the user has granted permission for the user to control camera PTZ.
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc b/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc
index 7a28460a..22974cf8 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.cc
@@ -169,6 +169,13 @@
     ScriptPromiseResolver<ImageBitmap>* resolver) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
+  // Hold a reference across `OnVideoFrame()` (`WeakPtr` only checks validity at
+  // callback entry). Creating or accessing the GPU context below can
+  // synchronously flush pending GPU channel callbacks, which may allocate on
+  // the Oilpan heap and trigger lazy sweeping of an unreachable `ImageCapture`
+  // owner (`~ImageCapture()`), dropping `ImageCapture::frame_grabber_`.
+  scoped_refptr<ImageCaptureFrameGrabber> keep_alive(this);
+
   auto required_provider_info = CreateSnapshotProviderInfoForVideoFrame(*frame);
 
   if (!cached_draw_info_ ||
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h b/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
index 5185908..23c5eee5 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture_frame_grabber.h
@@ -17,6 +17,7 @@
 #include "third_party/blink/renderer/platform/graphics/canvas_non_2d_resource_provider.h"
 #include "third_party/blink/renderer/platform/graphics/canvas_snapshot_info.h"
 #include "third_party/blink/renderer/platform/scheduler/public/post_cancellable_task.h"
+#include "third_party/blink/renderer/platform/wtf/ref_counted.h"
 
 class SkImage;
 
@@ -30,7 +31,9 @@
 // called. This method receives an incoming media::VideoFrame on a background
 // thread and converts it into the appropriate SkBitmap which is sent back to
 // OnSkBitmap(). This class is single threaded throughout.
-class ImageCaptureFrameGrabber final : public MediaStreamVideoSink {
+class ImageCaptureFrameGrabber final
+    : public RefCounted<ImageCaptureFrameGrabber>,
+      public MediaStreamVideoSink {
  public:
   // Helper class to ensure an ImageBitmap promise is always resolved.
   class ScopedPromiseResolver;
@@ -40,14 +43,14 @@
   ImageCaptureFrameGrabber(const ImageCaptureFrameGrabber&) = delete;
   ImageCaptureFrameGrabber& operator=(const ImageCaptureFrameGrabber&) = delete;
 
-  ~ImageCaptureFrameGrabber() override;
-
   void GrabFrame(MediaStreamComponent* component,
                  ScriptPromiseResolver<ImageBitmap>* resolver,
                  scoped_refptr<base::SingleThreadTaskRunner> task_runner,
                  base::TimeDelta timeout);
 
  private:
+  friend class RefCounted<ImageCaptureFrameGrabber>;
+  ~ImageCaptureFrameGrabber() override;
   // Internal class to receive, convert and forward one frame.
   class SingleShotFrameHandler;
 
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
index 6e22618..061a541a 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
@@ -5,11 +5,13 @@
 #include "third_party/blink/renderer/modules/imagecapture/image_capture.h"
 
 #include "base/functional/callback_helpers.h"
+#include "base/test/bind.h"
 #include "base/time/time.h"
 #include "media/base/video_frame.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/platform/modules/mediastream/web_media_stream_sink.h"
+#include "third_party/blink/public/platform/web_graphics_context_3d_provider.h"
 #include "third_party/blink/public/web/web_heap.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_function.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
@@ -36,6 +38,8 @@
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_track.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_video_capturer_source.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/testing/io_task_runner_testing_platform_support.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
 
@@ -1611,6 +1615,38 @@
   EXPECT_TRUE(tester.IsFulfilled());
 }
 
+TEST_F(ImageCaptureTest, GrabFrameSurvivesOwnerCollectionDuringConversion) {
+  V8TestingScope scope;
+  SetupTrackMocks(scope);
+  track_->SetReadyState(V8MediaStreamTrackState::Enum::kLive);
+  track_->setEnabled(true);
+  track_->SetMuted(false);
+
+  WeakPersistent<ImageCapture> weak_image_capture(image_capture_.Get());
+  SharedGpuContext::Reset();
+  base::ScopedClosureRunner reset_context(
+      base::BindOnce(&SharedGpuContext::Reset));
+  SharedGpuContext::SetContextProviderFactoryForTesting(
+      base::BindLambdaForTesting(
+          [&]() -> std::unique_ptr<WebGraphicsContext3DProvider> {
+            // GPU channel initialization can run callbacks which trigger GC.
+            if (image_capture_) {
+              image_capture_.Clear();
+              WebHeap::CollectAllGarbageForTesting();
+              EXPECT_FALSE(weak_image_capture);
+            }
+            return nullptr;
+          }));
+
+  auto result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_FALSE(weak_image_capture);
+  EXPECT_TRUE(tester.IsFulfilled());
+  EXPECT_EQ(0u, platform_track_->CountSinks());
+}
+
 TEST_F(ImageCaptureTest, GrabFrameOfMutedTrackIsFulfilled) {
   V8TestingScope scope;
   SetupTrackMocks(scope);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
index 6e22618..061a541a 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
@@ -5,11 +5,13 @@
 #include "third_party/blink/renderer/modules/imagecapture/image_capture.h"
 
 #include "base/functional/callback_helpers.h"
+#include "base/test/bind.h"
 #include "base/time/time.h"
 #include "media/base/video_frame.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/platform/modules/mediastream/web_media_stream_sink.h"
+#include "third_party/blink/public/platform/web_graphics_context_3d_provider.h"
 #include "third_party/blink/public/web/web_heap.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_function.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
@@ -36,6 +38,8 @@
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_track.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_video_capturer_source.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/testing/io_task_runner_testing_platform_support.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
 
@@ -1611,6 +1615,38 @@
   EXPECT_TRUE(tester.IsFulfilled());
 }
 
+TEST_F(ImageCaptureTest, GrabFrameSurvivesOwnerCollectionDuringConversion) {
+  V8TestingScope scope;
+  SetupTrackMocks(scope);
+  track_->SetReadyState(V8MediaStreamTrackState::Enum::kLive);
+  track_->setEnabled(true);
+  track_->SetMuted(false);
+
+  WeakPersistent<ImageCapture> weak_image_capture(image_capture_.Get());
+  SharedGpuContext::Reset();
+  base::ScopedClosureRunner reset_context(
+      base::BindOnce(&SharedGpuContext::Reset));
+  SharedGpuContext::SetContextProviderFactoryForTesting(
+      base::BindLambdaForTesting(
+          [&]() -> std::unique_ptr<WebGraphicsContext3DProvider> {
+            // GPU channel initialization can run callbacks which trigger GC.
+            if (image_capture_) {
+              image_capture_.Clear();
+              WebHeap::CollectAllGarbageForTesting();
+              EXPECT_FALSE(weak_image_capture);
+            }
+            return nullptr;
+          }));
+
+  auto result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_FALSE(weak_image_capture);
+  EXPECT_TRUE(tester.IsFulfilled());
+  EXPECT_EQ(0u, platform_track_->CountSinks());
+}
+
 TEST_F(ImageCaptureTest, GrabFrameOfMutedTrackIsFulfilled) {
   V8TestingScope scope;
   SetupTrackMocks(scope);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.