CVE-2026-7985
Overview
Background
- VEA factory
media/gpu/gpu_video_encode_accelerator_factory.ccbuilds the ordered list of platformVideoEncodeAccelerator(VEA) creation callbacks (VEAFactoryFunction) and queries supported encode profiles, running in the GPU process.- `base::NoDestructor` static
- a function-local static whose lifetime spans the process; here used to cache the factory-function list “composed once and then reused.”
- Lazy mutation of a static
- the pre-patch pattern of checking whether the static vector is empty and, if so,
push_back-ing the platform factories into it on first use. - Data race / UAF
- concurrent access from multiple threads to the shared static (reading while another thread mutates it, or copying by value) can corrupt it or leave dangling references, which the advisory classifies as a use-after-free.
Root Cause Analysis
The pre-patch GetVEAFactoryFunctions lazily populated a mutable function-local static vea_factory_functions by testing !vea_factory_functions->empty() and otherwise push_back-ing the platform factories, with no synchronization; concurrent callers on different threads could observe and mutate the vector mid-population, a data race on shared static state (the commit summary: “Fix data races in VEA factory static initializers… only initialize them once”).
The fix extracts list construction into a pure helper CreateVEAFactoryFunctions and initializes the base::NoDestructor<std::vector<VEAFactoryFunction>> static once from that helper’s return value, relying on C++ thread-safe static initialization instead of runtime mutation; the disable_accelerated_video_encode case now returns a separate empty static. Correspondingly, the caller (the encode-accelerator creation loop) is changed from copying the list by value (std::vector<...> create_vea_functions =) to holding a const reference, and the supported-profiles path is reworked so the second-attempt profile query is cached in its own base::NoDestructor static (second_try_profiles) rather than reassigning a shared *profiles, while the disable_accelerated_*_encode std::erase_if filtering is moved into the per-query helper so it operates on a fresh local list instead of mutating shared cached state. These changes remove the write-after-initialization races that could leave callers referencing corrupted or freed vector storage.
base::NoDestructor statics were being lazily mutated (and copied) without synchronization across threads; the fix makes them initialize exactly once from pure constructor helpers (CreateVEAFactoryFunctions, second_try_profiles) and hands callers a const&, eliminating the data race that could corrupt the vector and dangle references.Attack Path
- Trigger concurrent encoder setup Multiple WebCodecs/WebRTC encode requests reach the GPU process so that the VEA factory list and supported-profile queries are exercised from more than one thread near-simultaneously.
- Race on the lazy static
One thread reads
vea_factory_functions(or copies it) while another is stillpush_back-ing into it, a data race on the shared vector’s storage. - Corrupted / dangling vector
The race corrupts the vector’s internal state or invalidates storage a concurrent reader holds (e.g. reallocation during
push_back), leaving a dangling reference. - Use-after-free A thread iterates the factory list through the stale/freed storage, the use-after-free reported by the advisory (the exact racing threads and freed allocation are not pinned down by the diff).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/gpu/gpu_video_encode_accelerator_factory.cc |
modified | |
erase_ifmedia/gpu/gpu_video_encode_accelerator_factory.cc |
modified | |
formedia/gpu/gpu_video_encode_accelerator_factory.cc |
modified |
Files Changed
media/gpu/gpu_video_encode_accelerator_factory.cc
Audit Directions
- Lazily mutated function-local staticsAudit
static/base::NoDestructorobjects populated via anempty()-check-then-push_backpattern; these are races unless construction is done in the initializer under C++11 thread-safe static init. - By-value copies of shared cachesFlag callers that copy a shared cached container by value while other threads may be mutating it; prefer a
const&to a once-initialized static. - In-place mutation of cached resultsReview code that reassigns or
erase_ifs a shared cached*profiles/list on each call; move filtering onto per-call locals so cross-thread callers never observe partial mutation.
Patch
From b88cfae5b7b14a346569c232eb40c6074dd248d2 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Thu, 02 Apr 2026 12:51:33 -0700
Subject: [PATCH] media: Fix data races in VEA factory static initializers
We stopped changing static variables and only initialize them once.
Bug: 498352423
Change-Id: I0a3a1a97ca45431133bc1da63438b8bd52bd4ba0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7722803
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609439}
---
diff --git a/media/gpu/gpu_video_encode_accelerator_factory.cc b/media/gpu/gpu_video_encode_accelerator_factory.cc
index b09fa0e3..bc07f8e 100644
--- a/media/gpu/gpu_video_encode_accelerator_factory.cc
+++ b/media/gpu/gpu_video_encode_accelerator_factory.cc
@@ -162,43 +162,52 @@
using VEAFactoryFunction =
base::RepeatingCallback<std::unique_ptr<VideoEncodeAccelerator>()>;
-std::vector<VEAFactoryFunction> GetVEAFactoryFunctions(
+std::vector<VEAFactoryFunction> CreateVEAFactoryFunctions(
const gpu::GpuPreferences& gpu_preferences,
const gpu::GpuDriverBugWorkarounds& gpu_workarounds,
const gpu::GPUInfo::GPUDevice& gpu_device) {
+ std::vector<VEAFactoryFunction> funcs;
+#if BUILDFLAG(USE_VAAPI)
+ funcs.push_back(base::BindRepeating(&CreateVaapiVEA));
+#elif BUILDFLAG(USE_V4L2_CODEC)
+ funcs.push_back(base::BindRepeating(&CreateV4L2VEA));
+#endif
+
+#if BUILDFLAG(IS_ANDROID)
+ funcs.push_back(base::BindRepeating(&CreateAndroidVEA, gpu_workarounds));
+#endif
+#if BUILDFLAG(IS_MAC)
+ funcs.push_back(base::BindRepeating(&CreateVTVEA));
+#endif
+#if BUILDFLAG(IS_WIN)
+ funcs.push_back(
+ base::BindRepeating(&CreateD3D12VEA, gpu_workarounds, gpu_device));
+ funcs.push_back(base::BindRepeating(
+ &CreateMediaFoundationVEA, gpu_preferences, gpu_workarounds, gpu_device));
+#endif
+#if BUILDFLAG(IS_FUCHSIA)
+ funcs.push_back(base::BindRepeating(&CreateFuchsiaVEA));
+#endif
+ return funcs;
+}
+
+const std::vector<VEAFactoryFunction>& GetVEAFactoryFunctions(
+ const gpu::GpuPreferences& gpu_preferences,
+ const gpu::GpuDriverBugWorkarounds& gpu_workarounds,
+ const gpu::GPUInfo::GPUDevice& gpu_device) {
+ if (gpu_preferences.disable_accelerated_video_encode) {
+ static const base::NoDestructor<std::vector<VEAFactoryFunction>>
+ empty_vector;
+ return *empty_vector;
+ }
+
// Array of VEAFactoryFunctions potentially usable on the current platform.
// This list is ordered by priority, from most to least preferred, if
// applicable. This list is composed once and then reused.
static base::NoDestructor<std::vector<VEAFactoryFunction>>
- vea_factory_functions;
- if (gpu_preferences.disable_accelerated_video_encode)
- return *vea_factory_functions;
- if (!vea_factory_functions->empty()) {
- return *vea_factory_functions;
- }
+ vea_factory_functions(CreateVEAFactoryFunctions(
+ gpu_preferences, gpu_workarounds, gpu_device));
-#if BUILDFLAG(USE_VAAPI)
- vea_factory_functions->push_back(base::BindRepeating(&CreateVaapiVEA));
-#elif BUILDFLAG(USE_V4L2_CODEC)
- vea_factory_functions->push_back(base::BindRepeating(&CreateV4L2VEA));
-#endif
-
-#if BUILDFLAG(IS_ANDROID)
- vea_factory_functions->push_back(
- base::BindRepeating(&CreateAndroidVEA, gpu_workarounds));
-#endif
-#if BUILDFLAG(IS_MAC)
- vea_factory_functions->push_back(base::BindRepeating(&CreateVTVEA));
-#endif
-#if BUILDFLAG(IS_WIN)
- vea_factory_functions->push_back(
- base::BindRepeating(&CreateD3D12VEA, gpu_workarounds, gpu_device));
- vea_factory_functions->push_back(base::BindRepeating(
- &CreateMediaFoundationVEA, gpu_preferences, gpu_workarounds, gpu_device));
-#endif
-#if BUILDFLAG(IS_FUCHSIA)
- vea_factory_functions->push_back(base::BindRepeating(&CreateFuchsiaVEA));
-#endif
return *vea_factory_functions;
}
@@ -220,6 +229,34 @@
GpuVideoAcceleratorUtil::InsertUniqueEncodeProfiles(vea_profiles,
&profiles);
}
+
+ if (gpu_workarounds.disable_accelerated_av1_encode) {
+ std::erase_if(profiles, [](const auto& vea_profile) {
+ return vea_profile.profile >= AV1PROFILE_PROFILE_MAIN &&
+ vea_profile.profile <= AV1PROFILE_PROFILE_PRO;
+ });
+ }
+
+ if (gpu_workarounds.disable_accelerated_vp8_encode) {
+ std::erase_if(profiles, [](const auto& vea_profile) {
+ return vea_profile.profile == VP8PROFILE_ANY;
+ });
+ }
+
+ if (gpu_workarounds.disable_accelerated_vp9_encode) {
+ std::erase_if(profiles, [](const auto& vea_profile) {
+ return vea_profile.profile >= VP9PROFILE_PROFILE0 &&
+ vea_profile.profile <= VP9PROFILE_PROFILE3;
+ });
+ }
+
+ if (gpu_workarounds.disable_accelerated_h264_encode) {
+ std::erase_if(profiles, [](const auto& vea_profile) {
+ return vea_profile.profile >= H264PROFILE_MIN &&
+ vea_profile.profile <= H264PROFILE_MAX;
+ });
+ }
+
return profiles;
}
@@ -242,7 +279,7 @@
EncoderStatus initialization_err{
EncoderStatus::Codes::kEncoderInitializationError};
- std::vector<VEAFactoryFunction> create_vea_functions =
+ const std::vector<VEAFactoryFunction>& create_vea_functions =
GetVEAFactoryFunctions(gpu_preferences, gpu_workarounds, gpu_device);
for (const auto& create_vea : create_vea_functions) {
std::unique_ptr<VideoEncodeAccelerator> vea = create_vea.Run();
@@ -300,38 +337,17 @@
// (e.g. via udev) has happened instead.
if (profiles->empty()) {
VLOGF(1) << "Supported profiles empty, querying again...";
- *profiles = GetSupportedProfilesInternal(gpu_preferences, gpu_workarounds,
- gpu_device);
+ static base::NoDestructor<VideoEncodeAccelerator::SupportedProfiles>
+ second_try_profiles(GetSupportedProfilesInternal(
+ gpu_preferences, gpu_workarounds, gpu_device));
+ if (second_try_profiles->empty()) {
+ return GetSupportedProfilesInternal(gpu_preferences, gpu_workarounds,
+ gpu_device);
+ }
+ return *second_try_profiles;
}
#endif
- if (gpu_workarounds.disable_accelerated_av1_encode) {
- std::erase_if(*profiles, [](const auto& vea_profile) {
- return vea_profile.profile >= AV1PROFILE_PROFILE_MAIN &&
- vea_profile.profile <= AV1PROFILE_PROFILE_PRO;
- });
- }
-
- if (gpu_workarounds.disable_accelerated_vp8_encode) {
- std::erase_if(*profiles, [](const auto& vea_profile) {
- return vea_profile.profile == VP8PROFILE_ANY;
- });
- }
-
- if (gpu_workarounds.disable_accelerated_vp9_encode) {
- std::erase_if(*profiles, [](const auto& vea_profile) {
- return vea_profile.profile >= VP9PROFILE_PROFILE0 &&
- vea_profile.profile <= VP9PROFILE_PROFILE3;
- });
- }
-
- if (gpu_workarounds.disable_accelerated_h264_encode) {
- std::erase_if(*profiles, [](const auto& vea_profile) {
- return vea_profile.profile >= H264PROFILE_MIN &&
- vea_profile.profile <= H264PROFILE_MAX;
- });
- }
-
return *profiles;
}
Original Bug Report
Potential RCE in GPU process via unsynchronized static mutation in GpuVideoEncodeAcceleratorFactory
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 without the security team.
Overview: A data race in GpuVideoEncodeAcceleratorFactory allows a compromised renderer to trigger concurrent mutation of process-global static vectors. This leads to heap corruption, double-frees, and use-after-free in the GPU process, providing a path for a renderer-to-GPU sandbox escape.
Affected files:
media/gpu/gpu_video_encode_accelerator_factory.ccgpu/ipc/service/gpu_service_impl.ccmedia/mojo/services/mojo_video_encode_accelerator_provider.cc
Estimated timestamp from git blame: 2026-02-03
Summary
Multiple thread-safety issues exist in media/gpu/gpu_video_encode_accelerator_factory.cc due to unsynchronized access and mutation of process-global static variables. These functions can be invoked concurrently by multiple threads in the GPU process. This can be triggered by a compromised renderer through the viz.mojom.Gpu and media.mojom.VideoEncodeAcceleratorProvider Mojo interfaces.
Technical Details
1. The Threading Model
When a renderer requests a Video Encode Accelerator, the request is routed to the GPU process at viz::GpuServiceImpl::CreateVideoEncodeAcceleratorProvider (components/viz/service/gl/gpu_service_impl.cc:593).
Crucially, to prevent blocking the main GPU sequence, GpuServiceImpl allocates a new ThreadPool task runner for each provider request (except on Fuchsia where it reuses a single thread). On Windows, it creates a new COMSTATaskRunner; on other platforms, it creates a new SequencedTaskRunner or SingleThreadTaskRunner (gpu_service_impl.cc:614-621).
If a compromised renderer binds multiple viz.mojom.Gpu providers, it can simultaneously invoke methods on all of them, causing the GPU process to execute those methods concurrently across distinct physical threads in the ThreadPool.
2. Unsynchronized Initialization in GetVEAFactoryFunctions
The most critical race exists in GetVEAFactoryFunctions (media/gpu/gpu_video_encode_accelerator_factory.cc:165). This function lazily populates a process-global static vector of callbacks:
static base::NoDestructor<std::vector<VEAFactoryFunction>>
vea_factory_functions;
if (gpu_preferences.disable_accelerated_video_encode)
return *vea_factory_functions;
if (!vea_factory_functions->empty()) {
return *vea_factory_functions;
}
#if BUILDFLAG(USE_VAAPI)
vea_factory_functions->push_back(base::BindRepeating(&CreateVaapiVEA));
// ... more push_backs ...
While C++11 guarantees that the initialization of the NoDestructor static object (calling the std::vector default constructor) is thread-safe, it provides no synchronization for subsequent modifications.
If multiple threads concurrently enter this function while vea_factory_functions is empty, they will all pass the empty() check. They will then concurrently execute push_back. This causes concurrent vector reallocations and deterministic double-frees of the backing store.
Furthermore, the elements being added are base::RepeatingCallback objects. When the corrupted vector is returned by value at the end of the function (return *vea_factory_functions;), the copy constructor invokes AddRef() on the internal BindState of each callback. Because the backing store was concurrently reallocated and freed, these BindState pointers point to freed memory. The AddRef() operation therefore writes to freed memory, providing a highly reliable and exploitable Use-After-Free (UAF) primitive.
3. Unsynchronized Mutation in GetSupportedProfiles
A similar race exists in GpuVideoEncodeAcceleratorFactory::GetSupportedProfiles() (media/gpu/gpu_video_encode_accelerator_factory.cc:284).
static base::NoDestructor<VideoEncodeAccelerator::SupportedProfiles> profiles(
GetSupportedProfilesInternal(gpu_preferences, gpu_workarounds,
gpu_device));
// ...
if (gpu_workarounds.disable_accelerated_vp9_encode) {
std::erase_if(*profiles, [](const auto& vea_profile) {
return vea_profile.profile >= VP9PROFILE_PROFILE0 &&
vea_profile.profile <= VP9PROFILE_PROFILE3;
});
}
After thread-safe initialization, multiple threads can concurrently execute std::erase_if operations on the static vector based on GPU driver bug workarounds. Concurrent std::erase_if operations lead to data races on the vector’s internal state (e.g., size_ adjustments and memmove operations on the backing store), resulting in heap corruption or UAF during the copy-constructor return.
On V4L2 platforms, an additional race exists where *profiles = ... (gpu_video_encode_accelerator_factory.cc:303) can invoke concurrent std::vector::operator=, leading to deterministic double-frees of the backing store.
Potential Attack Surface and Trigger
The vulnerability is reachable from a compromised renderer. Suggested steps for an attacker:
- Bind
viz.mojom.Gpuvia theBrowserInterfaceBroker. - Call
Gpu.CreateVideoEncodeAcceleratorProvidermultiple times. Each call creates a new task runner in the GPU process thread pool. - Each provider is bound to its own task runner. The renderer then simultaneously calls
GetVideoEncodeAcceleratorSupportedProfilesorCreateVideoEncodeAcceleratoron all provider remotes. - These Mojo dispatches result in concurrent execution of the vulnerable static-accessing code in the GPU process, triggering the UAF or heap corruption.
Note: Our tooling agent has not executed these steps to provide a working proof-of-concept.
Impact
This is a renderer-to-GPU sandbox escape primitive. Successful exploitation allows for arbitrary code execution within the GPU process. The GPU process has a broader sandbox than the renderer, including direct access to driver ioctls and hardware acceleration APIs.
Suggested Fix
The simplest and most robust fix is to use a lock (base::Lock) inside GetVEAFactoryFunctions and GetSupportedProfiles to protect the initialization and mutation of the static variables. Alternatively, use base::OnceFlag to ensure the vector is populated exactly once before it can be read.
Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.