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

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
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.
Tracker498780188
Fix commitb7eadeee30fc (chromium/src) +24/-56
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-04-02 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-05-05
Patch gap25 days — fix public in source before the advisory

Background

`lose_context_when_out_of_memory`
a per-context flag that tells the GPU command-buffer client (GLES2Implementation, RasterImplementation) and the GPU-process decoder (gles2_decoder, raster_decoder) to treat an out-of-memory condition as a context loss instead of reporting an error and carrying on.
`GLESCreationAttribs` / `RasterCreationAttribs`
Mojo structs in gpu_channel.mojom that a GPU client sends to the GPU process when it creates a command buffer. Before the patch they carried lose_context_when_out_of_memory, so the client chose the decoder’s OOM policy.
`ContextProviderCommandBuffer`
the viz helper (CreateForGL, CreateForRaster) that renderer and browser code use to create GL or raster contexts over a GpuChannelHost.
Context loss
the command-buffer recovery path in which the context is torn down and its clients are notified, so no further commands run against partially-failed state.

Root Cause Analysis

Before the patch, whether the GPU decoder lost its context on OOM was decided by the client. GLES2CommandBufferStub and RasterCommandBufferStub passed attribs.lose_context_when_out_of_memory straight from the IPC-supplied ContextCreationAttribs into Initialize(), and InProcessCommandBuffer did the same. Several callers set the flag to false: the Android browser compositor, the Android BrowserGpuVideoAcceleratorFactories context, the RendererBlinkPlatformImpl raster context, RasterInProcessContext, GLInProcessContext, and the browser-test helper. In those contexts an allocation failure left the decoder or client running in a degraded, partially-initialized state instead of aborting. The advisory classes the result as a use-after-free in GPU. The diff does not show the specific object that is freed and then reused, so the exact sink is inferred: execution continues past an OOM failure.

The fix removes the field from both mojom structs and from every CreateForGL / CreateForRaster signature. It hard-codes /*lose_context_when_out_of_memory=*/true in every decoder Initialize() call and every GLES2Implementation / RasterImplementation constructor, so OOM always ends in a clean context loss and clients can no longer opt out.

Key insight
The GPU process let a less-trusted client choose, through IPC creation attributes, whether an out-of-memory error was fatal to the context. Continuing after OOM is the unsafe mode the UAF stems from (inferred). The fix removes the knob entirely and forces lose-context-on-OOM on both the service and client sides.

Attack Path

  1. Create a context with the opt-out A GPU client creates a command buffer with lose_context_when_out_of_memory=false, either through a legitimate code path that used false or through creation attributes a compromised renderer supplies itself (inferred). The pre-patch GLES2CommandBufferStub / RasterCommandBufferStub honored the value as sent.
  2. Drive the GPU to OOM The client issues allocations through GL or raster commands until a GPU-side allocation fails.
  3. Decoder continues past the failure Because the context is not lost, command processing continues in the GPU process with state left behind by the failed allocation.
  4. Use-after-free in GPU A later command touches the freed or stale object, which the advisory reports as a use-after-free (the precise object and command are not shown in the diff; inferred).

Impact Assessment

The advisory rates this a high-severity use-after-free in the GPU process. It is reachable by a GPU client: a web page through renderer contexts, or a compromised renderer that controls its creation attributes (inferred). Memory corruption in the GPU process could serve as a sandbox-escape step (inferred), but the patch demonstrates only the removal of the client-controlled OOM-continuation mode. The provided material does not mention in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/compositor_impl_android.cc
modified
if
content/renderer/render_thread_impl.cc
modified
if
gpu/ipc/in_process_command_buffer.cc
modified
if
gpu/ipc/service/gles2_command_buffer_stub.cc
modified
if
gpu/ipc/service/raster_command_buffer_stub.cc
modified
if
services/viz/public/cpp/gpu/context_provider_command_buffer.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
  • gpu/ipc/raster_in_process_context.cc
  • gpu/ipc/service/gles2_command_buffer_stub.cc
  • gpu/ipc/service/raster_command_buffer_stub.cc
  • services/viz/public/cpp/gpu/context_provider_command_buffer.cc
  • services/viz/public/cpp/gpu/context_provider_command_buffer.h
  • third_party/blink/renderer/platform/widget/widget_base.cc

Audit Directions

  • Client-controlled service policy
    In GPU, WebGPU and other Mojo creation-attribute structs, look for fields that select a safety or failure mode in the privileged process; the service should not let the client weaken error handling.
  • Continue-after-allocation-failure paths
    In gles2 and raster decoder handlers, check what happens when an allocation fails and the context is not lost: whether objects stay registered, whether texture or buffer maps are left half-updated, and whether a later command can reach freed state.
  • Divergent defaults across call sites
    Where a parameter defaults one way but some call sites (often Android or in-process paths) pass the opposite, check whether the less-tested combination is reachable from the renderer.
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..e510835 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/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();
       auto result =
-          raster_decoder->Initialize(attribs->lose_context_when_out_of_memory);
+          raster_decoder->Initialize(/*lose_context_when_out_of_memory=*/true);
       if (result != gpu::ContextResult::kSuccess) {
         DestroyOnGpuThread();
         DLOG(ERROR) << "Failed to initialize decoder.";
@@ -451,7 +450,7 @@
       }
       auto result = gles2_decoder->Initialize(
           surface, context_, /*offscreen=*/true, attribs->context_type,
-          attribs->lose_context_when_out_of_memory);
+          /*lose_context_when_out_of_memory=*/true);
       if (result != gpu::ContextResult::kSuccess) {
         DestroyOnGpuThread();
         DLOG(ERROR) << "Failed to initialize decoder.";
diff --git a/gpu/ipc/raster_in_process_context.cc b/gpu/ipc/raster_in_process_context.cc
index 926a50fa..166e6cd 100644
--- a/gpu/ipc/raster_in_process_context.cc
+++ b/gpu/ipc/raster_in_process_context.cc
@@ -45,10 +45,8 @@
     CommandBufferTaskExecutor* task_executor,
     gpu::raster::GrShaderCache* gr_shader_cache,
     GpuProcessShmCount* use_shader_cache_shm_count) {
-  constexpr bool lose_context_when_out_of_memory = false;
   auto attribs = mojom::ContextCreationAttribs::NewRaster(
-      mojom::RasterCreationAttribs::New(
-          /*lose_context_when_out_of_memory=*/lose_context_when_out_of_memory));
+      mojom::RasterCreationAttribs::New());
 
   command_buffer_ =
       std::make_unique<InProcessCommandBuffer>(task_executor, GURL());
@@ -74,8 +72,7 @@
 
   raster_implementation_ = std::make_unique<raster::RasterImplementation>(
       raster_helper.get(), transfer_buffer_.get(),
-      /*lose_context_when_out_of_memory=*/lose_context_when_out_of_memory,
-      command_buffer_.get());
+      /*lose_context_when_out_of_memory=*/true, command_buffer_.get());
   result = raster_implementation_->Initialize(memory_limits);
   raster_implementation_->SetLostContextCallback(base::BindOnce(
       []() { EXPECT_TRUE(false) << "Unexpected lost context."; }));
diff --git a/gpu/ipc/service/gles2_command_buffer_stub.cc b/gpu/ipc/service/gles2_command_buffer_stub.cc
index b71b3ce..53410f5 100644
--- a/gpu/ipc/service/gles2_command_buffer_stub.cc
+++ b/gpu/ipc/service/gles2_command_buffer_stub.cc
@@ -297,7 +297,7 @@
   // Initialize the decoder with either the view or pbuffer GLContext.
   auto result = gles2_decoder_->Initialize(
       surface_, context, /*offscreen=*/true, attribs.context_type,
-      attribs.lose_context_when_out_of_memory);
+      /*lose_context_when_out_of_memory=*/true);
   if (result != gpu::ContextResult::kSuccess) {
     DLOG(ERROR) << "Failed to initialize decoder.";
     return result;
diff --git a/gpu/ipc/service/raster_command_buffer_stub.cc b/gpu/ipc/service/raster_command_buffer_stub.cc
index 4afc11a..603158bb 100644
--- a/gpu/ipc/service/raster_command_buffer_stub.cc
+++ b/gpu/ipc/service/raster_command_buffer_stub.cc
@@ -59,8 +59,6 @@
   TRACE_EVENT0("gpu", "RasterBufferStub::Initialize");
   UpdateActiveUrl();
 
-  const auto& attribs = *init_params.attribs->get_raster();
-
   GpuChannelManager* manager = channel_->gpu_channel_manager();
   DCHECK(manager);
 
@@ -107,7 +105,7 @@
   }
 
   // Initialize the decoder with either the view or pbuffer GLContext.
-  result = decoder->Initialize(attribs.lose_context_when_out_of_memory);
+  result = decoder->Initialize(/*lose_context_when_out_of_memory=*/true);
   if (result != gpu::ContextResult::kSuccess) {
     DLOG(ERROR) << "Failed to initialize decoder.";
     return result;
diff --git a/services/viz/public/cpp/gpu/context_provider_command_buffer.cc b/services/viz/public/cpp/gpu/context_provider_command_buffer.cc
index ce86afe4..fc497204 100644
--- a/services/viz/public/cpp/gpu/context_provider_command_buffer.cc
+++ b/services/viz/public/cpp/gpu/context_provider_command_buffer.cc
@@ -102,10 +102,8 @@
     int32_t stream_id,
     gpu::SchedulingPriority stream_priority,
     const GURL& active_url,
-    command_buffer_metrics::ContextType type,
-    bool lose_context_when_out_of_memory) {
+    command_buffer_metrics::ContextType type) {
   auto attributes = gpu::mojom::GLESCreationAttribs::New();
-  attributes->lose_context_when_out_of_memory = lose_context_when_out_of_memory;
 
   return base::MakeRefCounted<ContextProviderCommandBuffer>(
       base::PassKey<ContextProviderCommandBuffer>(), std::move(channel),
@@ -176,10 +174,8 @@
     bool automatic_flushes,
     bool support_locking,
     const gpu::SharedMemoryLimits& memory_limits,
-    command_buffer_metrics::ContextType type,
-    bool lose_context_when_out_of_memory) {
+    command_buffer_metrics::ContextType type) {
   auto attributes = gpu::mojom::RasterCreationAttribs::New();
-  attributes->lose_context_when_out_of_memory = lose_context_when_out_of_memory;
 
   return base::MakeRefCounted<ContextProviderCommandBuffer>(
       base::PassKey<ContextProviderCommandBuffer>(), std::move(channel),
@@ -330,8 +326,7 @@
       DCHECK(channel_);
       auto raster_impl = std::make_unique<gpu::raster::RasterImplementation>(
           raster_helper.get(), transfer_buffer.get(),
-          attributes_->get_raster()->lose_context_when_out_of_memory,
-          command_buffer_.get());
+          /*lose_context_when_out_of_memory=*/true, command_buffer_.get());
       bind_result_ = raster_impl->Initialize(memory_limits_);
       if (bind_result_ != gpu::ContextResult::kSuccess) {
         DLOG(ERROR) << "Failed to initialize RasterImplementation.";
@@ -373,8 +368,7 @@
       // gpu::ContextSupport interface.
       auto gles2_impl = std::make_unique<gpu::gles2::GLES2Implementation>(
           gles2_helper.get(), /*share_group=*/nullptr, transfer_buffer.get(),
-          attributes_->get_gles()->lose_context_when_out_of_memory,
-          command_buffer_.get());
+          /*lose_context_when_out_of_memory=*/true, command_buffer_.get());
       bind_result_ = gles2_impl->Initialize(memory_limits_);
       if (bind_result_ != gpu::ContextResult::kSuccess) {
         DLOG(ERROR) << "Failed to initialize GLES2Implementation.";
diff --git a/services/viz/public/cpp/gpu/context_provider_command_buffer.h b/services/viz/public/cpp/gpu/context_provider_command_buffer.h
index bdafbc6a..f79e4dea 100644
--- a/services/viz/public/cpp/gpu/context_provider_command_buffer.h
+++ b/services/viz/public/cpp/gpu/context_provider_command_buffer.h
@@ -75,8 +75,7 @@
       int32_t stream_id,
       gpu::SchedulingPriority stream_priority,
       const GURL& active_url,
-      command_buffer_metrics::ContextType type,
-      bool lose_context_when_out_of_memory = false);
+      command_buffer_metrics::ContextType type);
 
   static scoped_refptr<ContextProviderCommandBuffer> CreateForWebGL(
       scoped_refptr<gpu::GpuChannelHost> channel,
@@ -93,8 +92,7 @@
       bool automatic_flushes,
       bool support_locking,
       const gpu::SharedMemoryLimits& memory_limits,
-      command_buffer_metrics::ContextType type,
-      bool lose_context_when_out_of_memory);
+      command_buffer_metrics::ContextType type);
 
   static scoped_refptr<ContextProviderCommandBuffer> CreateForWebGPU(
       scoped_refptr<gpu::GpuChannelHost> channel,
diff --git a/third_party/blink/renderer/platform/widget/widget_base.cc b/third_party/blink/renderer/platform/widget/widget_base.cc
index 3e76494..303e16d 100644
--- a/third_party/blink/renderer/platform/widget/widget_base.cc
+++ b/third_party/blink/renderer/platform/widget/widget_base.cc
@@ -869,13 +869,11 @@
 
   constexpr bool automatic_flushes = false;
   constexpr bool support_locking = false;
-  constexpr bool lose_context_when_out_of_memory = true;
 
   auto context_provider = viz::ContextProviderCommandBuffer::CreateForRaster(
       gpu_channel_host, kGpuStreamIdDefault, kGpuStreamPriorityDefault,
       GURL(params.url), automatic_flushes, support_locking, limits,
-      viz::command_buffer_metrics::ContextType::RENDERER_COMPOSITOR,
-      lose_context_when_out_of_memory);
+      viz::command_buffer_metrics::ContextType::RENDERER_COMPOSITOR);
 
 #if BUILDFLAG(IS_ANDROID)
   if (Platform::Current()->IsSynchronousCompositingEnabledForAndroidWebView() &&
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…

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