CVE-2026-7985
Overview
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
Patch
From b88cfae5b7b14a346569c232eb40c6074dd248d2 Mon Sep 17 00:00:00 2001 From: Eugene Zemtsov <[email protected]> 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 <[email protected]> Commit-Queue: Eugene Zemtsov <[email protected]> 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.