Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Tracker498352423
Fix commitb88cfae5b7b1 (chromium/src) +75/-59
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
media/gpu/gpu_video_encode_accelerator_factory.cc
modified
erase_if
media/gpu/gpu_video_encode_accelerator_factory.cc
modified
for
media/gpu/gpu_video_encode_accelerator_factory.cc
modified

Files Changed

  • media/gpu/gpu_video_encode_accelerator_factory.cc
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;
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • gpu/ipc/service/gpu_service_impl.cc
  • media/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:

  1. Bind viz.mojom.Gpu via the BrowserInterfaceBroker.
  2. Call Gpu.CreateVideoEncodeAcceleratorProvider multiple times. Each call creates a new task runner in the GPU process thread pool.
  3. Each provider is bound to its own task runner. The renderer then simultaneously calls GetVideoEncodeAcceleratorSupportedProfiles or CreateVideoEncodeAccelerator on all provider remotes.
  4. 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.

View on issue tracker