Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Sub-componentMedia
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498352423
Fix commitb88cfae5b7b1 (chromium/src) +75/-59
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

Background

VEA factory
media/gpu/gpu_video_encode_accelerator_factory.cc builds the ordered list of platform VideoEncodeAccelerator (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.

Key insight
Shared 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

  1. 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.
  2. Race on the lazy static One thread reads vea_factory_functions (or copies it) while another is still push_back-ing into it, a data race on the shared vector’s storage.
  3. 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.
  4. 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

An attacker-controlled page issuing concurrent video-encode operations can race the GPU process’s VEA factory initialization, corrupting a shared static and producing a use-after-free in the GPU process. The precondition is triggering concurrent encoder-factory/profile access; exploitation depends on winning the race, so it is non-deterministic. The vendor advisory rates this medium severity (“Use after free in GPU”) and does not report in-the-wild exploitation; escalation beyond the memory-safety fault is (inferred).

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

Audit Directions

  • Lazily mutated function-local statics
    Audit static/base::NoDestructor objects populated via an empty()-check-then-push_back pattern; these are races unless construction is done in the initializer under C++11 thread-safe static init.
  • By-value copies of shared caches
    Flag 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 results
    Review 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.
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