Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebCodecs
DescriptionUse after free in WebCodecs
ComponentWebCodecs
Bug ClassUAF
Tracker514715455
Fix commit6fd20a57fccf (chromium/src) +7/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/heap/heap_barrier_callback.h
modified

Files Changed

  • third_party/blink/renderer/platform/heap/heap_barrier_callback.h
From 6fd20a57fccfaf70f2b472c7f1246033011af527 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Wed, 20 May 2026 12:25:14 -0700
Subject: [PATCH] blink: Make HeapBarrierCallback thread-safe by default

HeapBarrierCallback was using Persistent to store aggregated results.
Since Persistent handles are thread-affine, destroying or clearing them
on another thread (e.g. when a PostTask fails due to worker thread
shutdown) results in free-list corruption of the calling thread's
PersistentRegion.

This change replaces Persistent with UnwrappingCrossThreadHandle to
ensure safe cross-thread destruction while strictly enforcing that the
underlying vector is only dereferenced on its creation thread,
preventing data races.

Bug: 514715455
Change-Id: I44930a8d0d742a6194c6380c7b94abc8cf03dab7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7860937
Reviewed-by: Nate Chapin <japhet@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633752}
---

diff --git a/third_party/blink/renderer/platform/heap/heap_barrier_callback.h b/third_party/blink/renderer/platform/heap/heap_barrier_callback.h
index 80a9f738..54c8c24 100644
--- a/third_party/blink/renderer/platform/heap/heap_barrier_callback.h
+++ b/third_party/blink/renderer/platform/heap/heap_barrier_callback.h
@@ -18,6 +18,7 @@
 #include "base/notreached.h"
 #include "base/thread_annotations.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
 
 namespace blink {
 
@@ -29,24 +30,25 @@
   BarrierCallbackInfo(wtf_size_t num_callbacks,
                       base::OnceCallback<void(DoneArg)> done_callback)
       : num_callbacks_left_(num_callbacks),
-        results_(MakeGarbageCollected<GCedHeapVector<Member<T>>>()),
+        results_(MakeCrossThreadHandle(
+            MakeGarbageCollected<GCedHeapVector<Member<T>>>())),
         done_callback_(std::move(done_callback)) {
-    results_->reserve(num_callbacks);
+    results_.GetOnCreationThread()->reserve(num_callbacks);
   }
 
   void Run(T* t) {
     DCHECK_NE(num_callbacks_left_, 0U);
-    results_->push_back(std::move(t));
+    results_.GetOnCreationThread()->push_back(std::move(t));
     --num_callbacks_left_;
 
     if (num_callbacks_left_ == 0) {
-      std::move(done_callback_).Run(*results_.Get());
+      std::move(done_callback_).Run(*results_.GetOnCreationThread());
     }
   }
 
  private:
   wtf_size_t num_callbacks_left_;
-  Persistent<GCedHeapVector<Member<T>>> results_;
+  UnwrappingCrossThreadHandle<GCedHeapVector<Member<T>>> results_;
   base::OnceCallback<void(DoneArg)> done_callback_;
 };
 
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential cross-thread Use-After-Free in VideoEncoder via HeapBarrierCallback

Flapjack, 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: A thread-affinity violation occurs when VideoEncoder.isConfigSupported() is called from a Web Worker that is subsequently terminated. A garbage-collected Persistent handle is inadvertently destroyed on the main thread when a PostTask fails, potentially corrupting the main thread’s PersistentRegion free-list if the worker’s memory pages have been recycled. This memory corruption can potentially be exploited for Remote Code Execution (RCE) in the renderer process.

Affected files:

  • third_party/blink/renderer/modules/webcodecs/video_encoder.cc
  • third_party/blink/renderer/platform/heap/heap_barrier_callback.h
  • third_party/blink/renderer/modules/webcodecs/gpu_factories_retriever.cc
  • media/mojo/clients/mojo_codec_factory.cc

Estimated timestamp from git blame: 2024-02-29

Summary

A potential memory corruption vulnerability exists in VideoEncoder::isConfigSupported due to the improper cross-thread handling of cppgc::Persistent handles.

When isConfigSupported is called from a Web Worker, it utilizes HeapBarrierCallback to aggregate results. This callback captures a thread-affine Persistent handle but is wrapped in a type-erased base::RepeatingCallback. This circumvents CrossThreadBindOnce’s static safety checks, allowing the callback to be safely passed to the main thread via RetrieveGpuFactoriesWithKnownEncoderSupport.

The callback is registered in MojoCodecFactory using base::BindPostTaskToCurrentDefault. If the originating worker thread is terminated before the GPU factory response is received, the worker’s task runner is invalidated. When the main thread attempts to execute the BindPostTask trampoline, the underlying PostTask fails. In Chromium, a failed PostTask results in the synchronous destruction of the passed closure on the calling thread. Consequently, the Persistent handle created on the worker thread is destroyed on the main thread.

Technical Details

  1. HeapBarrierCallback (third_party/blink/renderer/platform/heap/heap_barrier_callback.h) stores results in a BarrierCallbackInfo which holds a Persistent<GCedHeapVector>.
  2. This BarrierCallbackInfo is captured by value in a base::RepeatingCallback and sent across threads to MojoCodecFactory::NotifyEncoderSupportKnown, bypassing Blink’s thread-affinity checks.
  3. MojoCodecFactory wraps this in a BindPostTaskTrampoline to ensure it runs on the worker thread.
  4. If the worker terminates, its task runner shuts down and its Oilpan HeapBase and PersistentRegion are destroyed. The backing memory for PersistentNodeSlots is freed to the system allocator.
  5. When the main thread calls .Run() on the trampoline, task_runner_->PostTask fails and immediately destroys the closure on the main thread.
  6. The destruction sequence calls Persistent::Clear(), which invokes WeaknessPolicy::GetPersistentRegion(GetValue()).FreeNode(GetNode()).
  7. GetPersistentRegion locates the region by inspecting the BasePage of the pointee (GetValue()). If PartitionAlloc has recycled the worker’s freed 128KB page into the main thread’s Oilpan heap, BasePage::FromPayload will return the main thread’s HeapBase.
  8. FreeNode executes on the main thread’s PersistentRegion. Because the thread-check is a debug-only CPPGC_DCHECK, execution proceeds without locks in release builds.
  9. FreeNode blindly inserts the dangling GetNode() pointer (which belonged to the worker’s freed memory) into the main thread’s free_list_head_.
  10. Subsequent Persistent allocations on the main thread will pop this attacker-controlled pointer, leading to arbitrary memory writes (Write-Where primitive) when InitializeAsUsedNode is called.

Potential Steps to Reproduce

Note: These are suggested steps; our tooling agent does not have the ability to run code to verify them.

  1. Spawn a Web Worker from a malicious webpage.
  2. In the worker, call VideoEncoder.isConfigSupported(config) with a configuration that requires hardware detection (e.g., leaving hw_pref unset or setting it to prefer-hardware).
  3. Immediately terminate the worker (self.close() or worker.terminate()).
  4. Perform heap grooming on the main thread to encourage Oilpan page recycling.
  5. When the hardware detection completes asynchronously, the main thread attempts to post the callback, fails, and destroys the Persistent handle, corrupting its own free-list.

Suggested Fix

HeapBarrierCallback should not encapsulate Persistent handles if the resulting callback is intended to be moved across threads.

  1. Modify BarrierCallbackInfo in third_party/blink/renderer/platform/heap/heap_barrier_callback.h to use CrossThreadPersistent instead of Persistent.
  2. Alternatively, ensure that VideoEncoder::isConfigSupported utilizes CrossThreadHandle and explicitly unwraps the handle only when safely back on the worker thread, avoiding the capture of garbage-collected pointers or Persistent handles inside the HeapBarrierCallback that is passed across thread boundaries.

Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0


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