CVE-2026-10013
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/heap/heap_barrier_callback.h |
modified |
Files Changed
third_party/blink/renderer/platform/heap/heap_barrier_callback.h
Patch
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_;
};
Original Bug Report
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.ccthird_party/blink/renderer/platform/heap/heap_barrier_callback.hthird_party/blink/renderer/modules/webcodecs/gpu_factories_retriever.ccmedia/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
HeapBarrierCallback(third_party/blink/renderer/platform/heap/heap_barrier_callback.h) stores results in aBarrierCallbackInfowhich holds aPersistent<GCedHeapVector>.- This
BarrierCallbackInfois captured by value in abase::RepeatingCallbackand sent across threads toMojoCodecFactory::NotifyEncoderSupportKnown, bypassing Blink’s thread-affinity checks. MojoCodecFactorywraps this in aBindPostTaskTrampolineto ensure it runs on the worker thread.- If the worker terminates, its task runner shuts down and its Oilpan
HeapBaseandPersistentRegionare destroyed. The backing memory forPersistentNodeSlotsis freed to the system allocator. - When the main thread calls
.Run()on the trampoline,task_runner_->PostTaskfails and immediately destroys the closure on the main thread. - The destruction sequence calls
Persistent::Clear(), which invokesWeaknessPolicy::GetPersistentRegion(GetValue()).FreeNode(GetNode()). GetPersistentRegionlocates the region by inspecting theBasePageof the pointee (GetValue()). If PartitionAlloc has recycled the worker’s freed 128KB page into the main thread’s Oilpan heap,BasePage::FromPayloadwill return the main thread’sHeapBase.FreeNodeexecutes on the main thread’sPersistentRegion. Because the thread-check is a debug-onlyCPPGC_DCHECK, execution proceeds without locks in release builds.FreeNodeblindly inserts the danglingGetNode()pointer (which belonged to the worker’s freed memory) into the main thread’sfree_list_head_.- Subsequent
Persistentallocations on the main thread will pop this attacker-controlled pointer, leading to arbitrary memory writes (Write-Whereprimitive) whenInitializeAsUsedNodeis called.
Potential Steps to Reproduce
Note: These are suggested steps; our tooling agent does not have the ability to run code to verify them.
- Spawn a Web Worker from a malicious webpage.
- In the worker, call
VideoEncoder.isConfigSupported(config)with a configuration that requires hardware detection (e.g., leavinghw_prefunset or setting it toprefer-hardware). - Immediately terminate the worker (
self.close()orworker.terminate()). - Perform heap grooming on the main thread to encourage Oilpan page recycling.
- When the hardware detection completes asynchronously, the main thread attempts to post the callback, fails, and destroys the
Persistenthandle, corrupting its own free-list.
Suggested Fix
HeapBarrierCallback should not encapsulate Persistent handles if the resulting callback is intended to be moved across threads.
- Modify
BarrierCallbackInfointhird_party/blink/renderer/platform/heap/heap_barrier_callback.hto useCrossThreadPersistentinstead ofPersistent. - Alternatively, ensure that
VideoEncoder::isConfigSupportedutilizesCrossThreadHandleand explicitly unwraps the handle only when safely back on the worker thread, avoiding the capture of garbage-collected pointers orPersistenthandles inside theHeapBarrierCallbackthat 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.