Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Tracker498780188
Fix commitb7eadeee30fc (chromium/src) +24/-56
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/compositor_impl_android.cc
modified
if
content/renderer/render_thread_impl.cc
modified

Files Changed

  • content/browser/compositor/viz_process_transport_factory.cc
  • content/browser/media/android/browser_gpu_video_accelerator_factories.cc
  • content/browser/renderer_host/compositor_impl_android.cc
  • content/renderer/render_thread_impl.cc
  • content/renderer/renderer_blink_platform_impl.cc
  • content/test/gpu_browsertest_helpers.cc
  • gpu/ipc/common/gpu_channel.mojom
  • gpu/ipc/gl_in_process_context.cc
  • gpu/ipc/in_process_command_buffer.cc
From b7eadeee30fc0345055eaa9cea305e4d97fe8b08 Mon Sep 17 00:00:00 2001
From: Shrek Shao <shrekshao@google.com>
Date: Fri, 10 Apr 2026 15:36:17 -0700
Subject: [PATCH] Remove lose_context_when_out_of_memory from the context attribs

GL state becomes undefined once an out of memory error has occurred and
is not recoverable. Exposing this feature did not practically add any
recovery path and exposed state tracking issues when GL objects enter
undefined states due to OOM.

Bug: 498780188
Change-Id: I85c333b4ec4b5bf170e246535dc700a43ee85af4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7731172
Reviewed-by: Stefan Zager <szager@chromium.org>
Commit-Queue: Stefan Zager <szager@chromium.org>
Auto-Submit: Shrek Shao <shrekshao@google.com>
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Alex Gough <ajgo@chromium.org>
Reviewed-by: Xiaohan Wang <xhwang@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1613139}
---

diff --git a/content/browser/compositor/viz_process_transport_factory.cc b/content/browser/compositor/viz_process_transport_factory.cc
index 59a7d484..3b6170d 100644
--- a/content/browser/compositor/viz_process_transport_factory.cc
+++ b/content/browser/compositor/viz_process_transport_factory.cc
@@ -78,8 +78,7 @@
   GURL url("chrome://gpu/VizProcessTransportFactory::CreateContextProvider");
   return viz::ContextProviderCommandBuffer::CreateForRaster(
       std::move(gpu_channel_host), kGpuStreamIdDefault, kGpuStreamPriorityUI,
-      std::move(url), kAutomaticFlushes, supports_locking, memory_limits, type,
-      /*lose_context_when_out_of_memory=*/true);
+      std::move(url), kAutomaticFlushes, supports_locking, memory_limits, type);
 }
 
 bool IsContextLost(viz::RasterContextProvider* context_provider) {
diff --git a/content/browser/media/android/browser_gpu_video_accelerator_factories.cc b/content/browser/media/android/browser_gpu_video_accelerator_factories.cc
index 83f57048..a8059687 100644
--- a/content/browser/media/android/browser_gpu_video_accelerator_factories.cc
+++ b/content/browser/media/android/browser_gpu_video_accelerator_factories.cc
@@ -36,8 +36,7 @@
            "CreateGpuVideoAcceleratorFactories"),
       automatic_flushes, support_locking,
       gpu::SharedMemoryLimits::ForMailboxContext(),
-      viz::command_buffer_metrics::ContextType::UNKNOWN,
-      /*lose_context_when_out_of_memory=*/false);
+      viz::command_buffer_metrics::ContextType::UNKNOWN);
   context_provider->BindToCurrentSequence();
 
   auto gpu_factories = std::make_unique<BrowserGpuVideoAcceleratorFactories>(
diff --git a/content/browser/renderer_host/compositor_impl_android.cc b/content/browser/renderer_host/compositor_impl_android.cc
index 128da9eb..7cc0c5e 100644
--- a/content/browser/renderer_host/compositor_impl_android.cc
+++ b/content/browser/renderer_host/compositor_impl_android.cc
@@ -514,10 +514,7 @@
       GURL("chrome://gpu/CompositorImpl::CompositorContextProvider"),
       automatic_flushes, support_locking,
       GetCompositorContextSharedMemoryLimits(root_window_),
-      viz::command_buffer_metrics::ContextType::BROWSER_COMPOSITOR,
-      /*lose_context_when_out_of_memory=*/false
-
-  );
+      viz::command_buffer_metrics::ContextType::BROWSER_COMPOSITOR);
   auto result = context_provider->BindToCurrentSequence();
 
   if (result == gpu::ContextResult::kFatalFailure) {
diff --git a/content/renderer/render_thread_impl.cc b/content/renderer/render_thread_impl.cc
index 40f7d3d..8d193858 100644
--- a/content/renderer/render_thread_impl.cc
+++ b/content/renderer/render_thread_impl.cc
@@ -930,8 +930,7 @@
   auto media_context_provider = viz::ContextProviderCommandBuffer::CreateForGL(
       gpu_channel_host, kGpuStreamIdMedia, kGpuStreamPriorityMedia,
       GURL("chrome://gpu/RenderThreadImpl::CreateOffscreenContext/Media"),
-      viz::command_buffer_metrics::ContextType::MEDIA,
-      /*lose_context_when_out_of_memory=*/true);
+      viz::command_buffer_metrics::ContextType::MEDIA);
 
   const bool enable_video_decode_accelerator =
 #if BUILDFLAG(IS_LINUX)
@@ -1015,9 +1014,7 @@
           GURL("chrome://gpu/RenderThreadImpl::CreateOffscreenContext/"
                "RenderCompositor"),
           /*automatic_flushes=*/false, /*support_locking=*/false, limits,
-          viz::command_buffer_metrics::ContextType::RENDERER_COMPOSITOR,
-          /*lose_context_when_out_of_memory=*/true);
-
+          viz::command_buffer_metrics::ContextType::RENDERER_COMPOSITOR);
   return video_frame_compositor_context_provider_;
 }
 
@@ -1081,8 +1078,7 @@
                "RendererMainThread"),
           /*automatic_flushes=*/true, /*support_locking=*/false,
           gpu::SharedMemoryLimits(),
-          viz::command_buffer_metrics::ContextType::RENDERER_MAIN_THREAD,
-          /*lose_context_when_out_of_memory=*/true);
+          viz::command_buffer_metrics::ContextType::RENDERER_MAIN_THREAD);
 
   auto result = shared_main_thread_contexts_->BindToCurrentSequence();
   if (result != gpu::ContextResult::kSuccess) {
@@ -1537,8 +1533,7 @@
                "RenderWorker"),
           /*automatic_flushes=*/false, /*support_locking=*/true,
           shared_memory_limits,
-          viz::command_buffer_metrics::ContextType::RENDERER_RASTER_WORKER,
-          /*lose_context_when_out_of_memory=*/true);
+          viz::command_buffer_metrics::ContextType::RENDERER_RASTER_WORKER);
 
   auto result = shared_worker_context_provider_->BindToCurrentSequence();
   if (result != gpu::ContextResult::kSuccess) {
@@ -1580,8 +1575,7 @@
            "MediaWorker"),
       /*automatic_flushes=*/false, /*support_locking=*/true,
       shared_memory_limits,
-      viz::command_buffer_metrics::ContextType::RENDERER_MEDIA_WORKER,
-      /*lose_context_when_out_of_memory=*/true);
+      viz::command_buffer_metrics::ContextType::RENDERER_MEDIA_WORKER);
 
   GetMediaSequencedTaskRunner()->PostTaskAndReplyWithResult(
       FROM_HERE,
diff --git a/content/renderer/renderer_blink_platform_impl.cc b/content/renderer/renderer_blink_platform_impl.cc
index 020b881..e5108355 100644
--- a/content/renderer/renderer_blink_platform_impl.cc
+++ b/content/renderer/renderer_blink_platform_impl.cc
@@ -781,7 +781,6 @@
 
   constexpr bool automatic_flushes = true;
   constexpr bool support_locking = false;
-  constexpr bool lose_context_when_out_of_memory = false;
 
   gpu::SchedulingPriority stream_priority =
       (base::FeatureList::IsEnabled(features::kInitialWebUI) &&
@@ -795,8 +794,7 @@
       viz::ContextProviderCommandBuffer::CreateForRaster(
           std::move(gpu_channel_host), kGpuStreamIdDefault, stream_priority,
           GURL(document_url), automatic_flushes, support_locking,
-          gpu::SharedMemoryLimits(), ToVizContextType(context_type),
-          lose_context_when_out_of_memory));
+          gpu::SharedMemoryLimits(), ToVizContextType(context_type)));
 }
 
 //------------------------------------------------------------------------------
diff --git a/content/test/gpu_browsertest_helpers.cc b/content/test/gpu_browsertest_helpers.cc
index 1f01434..f471faf7 100644
--- a/content/test/gpu_browsertest_helpers.cc
+++ b/content/test/gpu_browsertest_helpers.cc
@@ -56,8 +56,7 @@
         content::kGpuStreamPriorityDefault, GURL(),
         /*automatic_flushes=*/false, /*support_locking=*/false,
         gpu::SharedMemoryLimits(),
-        viz::command_buffer_metrics::ContextType::FOR_TESTING,
-        /*lose_context_when_out_of_memory=*/false);
+        viz::command_buffer_metrics::ContextType::FOR_TESTING);
   }
 
   return viz::ContextProviderCommandBuffer::CreateForGL(
diff --git a/gpu/ipc/common/gpu_channel.mojom b/gpu/ipc/common/gpu_channel.mojom
index 661d954..e94b354 100644
--- a/gpu/ipc/common/gpu_channel.mojom
+++ b/gpu/ipc/common/gpu_channel.mojom
@@ -40,13 +40,11 @@
 };
 
 struct RasterCreationAttribs {
-  bool lose_context_when_out_of_memory = false;
 };
 
 struct GLESCreationAttribs {
   gl.mojom.GpuPreference gpu_preference = kLowPower;
   bool fail_if_major_perf_caveat = false;
-  bool lose_context_when_out_of_memory = false;
   ContextType context_type = kOpenGLES2;
 };
 
diff --git a/gpu/ipc/gl_in_process_context.cc b/gpu/ipc/gl_in_process_context.cc
index ccc527de..b92d9bc 100644
--- a/gpu/ipc/gl_in_process_context.cc
+++ b/gpu/ipc/gl_in_process_context.cc
@@ -88,7 +88,7 @@
   // Create the object exposing the OpenGL API.
   gles2_implementation_ = std::make_unique<gles2::GLES2Implementation>(
       gles2_helper_.get(), /*share_group=*/nullptr, transfer_buffer_.get(),
-      /*lose_context_when_out_of_memory=*/false, command_buffer_.get());
+      /*lose_context_when_out_of_memory=*/true, command_buffer_.get());
 
   result = gles2_implementation_->Initialize(mem_limits);
   return result;
diff --git a/gpu/ipc/in_process_command_buffer.cc b/gpu/ipc/in_process_command_buffer.cc
index e99a578..115e4d67 100644
--- a/gpu/ipc/in_process_command_buffer.cc
+++ b/gpu/ipc/in_process_command_buffer.cc
@@ -362,9 +362,8 @@
               task_executor_->shared_image_manager(), context_state_,
               /*is_privileged=*/true);
 
-      const auto& attribs = params.attribs->get_raster();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/test/gpu_browsertest_helpers.cc b/content/test/gpu_browsertest_helpers.cc
index 1f01434..f471faf7 100644
--- a/content/test/gpu_browsertest_helpers.cc
+++ b/content/test/gpu_browsertest_helpers.cc
@@ -56,8 +56,7 @@
         content::kGpuStreamPriorityDefault, GURL(),
         /*automatic_flushes=*/false, /*support_locking=*/false,
         gpu::SharedMemoryLimits(),
-        viz::command_buffer_metrics::ContextType::FOR_TESTING,
-        /*lose_context_when_out_of_memory=*/false);
+        viz::command_buffer_metrics::ContextType::FOR_TESTING);
   }
 
   return viz::ContextProviderCommandBuffer::CreateForGL(
Loading diff…

Original Bug Report

reported by vm...@google.com

Write-after-unmap in GLES2 passthrough decoder via DoBufferData error path

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 potential logic error in GLES2DecoderPassthroughImpl::DoBufferData skips cleaning up internal mapped buffer state when an allocation error occurs in ANGLE. This leaves a dangling pointer to unmapped driver memory, which can be written to via a subsequent glUnmapBuffer command. A compromised renderer could exploit this to achieve arbitrary code execution in the GPU process, escaping the sandbox.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc

Estimated timestamp from git blame: 2021-06-30

Description

A potential write-after-unmap vulnerability exists in gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc within GLES2DecoderPassthroughImpl::DoBufferData.

When glBufferData is called on an already-mapped buffer, the OpenGL ES specification dictates an implicit unmap. ANGLE handles this unmap before attempting to allocate the new buffer.

If the new allocation fails (e.g., triggering a GL_OUT_OF_MEMORY because of an impossibly large size request during robust resource initialization), DoBufferData catches the error via CheckErrorCallbackState() and executes an early return. Crucially, this early return skips the cleanup code (resources_->mapped_buffer_map.erase(...)) that removes the buffer’s mapping information from the decoder’s state.

Normally, an OOM error destroys the context. However, a compromised renderer can bypass this by setting lose_context_when_out_of_memory to false in the ContextCreationAttribs during the initial context creation IPC.

Because the context survives and the mapping is not erased, the decoder retains a stale pointer to the implicitly unmapped driver memory. The attacker can then trigger DoUnmapBuffer, which copies attacker-controlled data from shared memory into this dangling driver pointer before unmapping.

Impact

This issue provides a powerful Write-After-Free (Write-After-Unmap) primitive. Because the dangling pointer points to driver-allocated memory (e.g., Vulkan, Metal, or D3D11) rather than PartitionAlloc memory, it is not protected by MiraclePtr (BackupRefPtr). A compromised renderer could potentially exploit this to overwrite adjacent driver structures and achieve Remote Code Execution (RCE) in the highly privileged GPU process, escaping the sandbox.

Suggested Exploitation Steps

Note: These are potential steps based on code analysis; our tooling cannot yet execute a working Proof of Concept.

  1. A compromised renderer process creates a new GLES context, setting lose_context_when_out_of_memory = false via the ContextCreationAttribs IPC payload.
  2. The attacker allocates a buffer and maps it for writing using glMapBufferRange. Chrome safely stores the driver’s mapping pointer in mapped_buffer_map.
  3. The attacker issues a glBufferData command for the exact same buffer with a massive size and a nullptr data argument.
  4. Inside ANGLE, the buffer is implicitly unmapped (freeing the driver memory). ANGLE then attempts to allocate a zero-filled buffer of the massive size, which fails and returns a GL_OUT_OF_MEMORY error.
  5. Chrome intercepts the error. Because context loss on OOM was disabled in Step 1, the context safely survives. DoBufferData early-returns, abandoning the cleanup and leaving the stale driver pointer in mapped_buffer_map.
  6. The attacker populates the shared memory associated with the mapping with a malicious payload.
  7. The attacker calls glUnmapBuffer (or glFlushMappedBufferRange). Chrome retrieves the stale pointer and performs a memcpy of the attacker’s payload directly into the freed driver memory.

Suggested Fix

The cleanup of mapped_buffer_map should not be conditionally bypassed if an error occurs. In GLES2DecoderPassthroughImpl::DoBufferData, the buffer mapping should be erased unconditionally if the API call is made, as the implicit unmap happens in ANGLE regardless of whether the subsequent buffer allocation succeeds.

For example, move the erase operation before the error check:

  CheckErrorCallbackState();
  api()->glBufferDataFn(target, size, data, usage);

  // Calling buffer data on a mapped buffer will implicitly unmap it in the driver.
  // We must unconditionally remove it from our tracking map, even if the allocation below failed.
  resources_->mapped_buffer_map.erase(bound_buffers_[target]);

  if (CheckErrorCallbackState()) {
    return error::kNoError;
  }

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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