CVE-2026-7918
Overview
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.mojomthat a GPU client sends to the GPU process when it creates a command buffer. Before the patch they carriedlose_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 aGpuChannelHost. - 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.
Attack Path
- 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 usedfalseor through creation attributes a compromised renderer supplies itself (inferred). The pre-patchGLES2CommandBufferStub/RasterCommandBufferStubhonored the value as sent. - Drive the GPU to OOM The client issues allocations through GL or raster commands until a GPU-side allocation fails.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/compositor_impl_android.cc |
modified | |
ifcontent/renderer/render_thread_impl.cc |
modified | |
ifgpu/ipc/in_process_command_buffer.cc |
modified | |
ifgpu/ipc/service/gles2_command_buffer_stub.cc |
modified | |
ifgpu/ipc/service/raster_command_buffer_stub.cc |
modified | |
ifservices/viz/public/cpp/gpu/context_provider_command_buffer.cc |
modified |
Files Changed
content/browser/compositor/viz_process_transport_factory.cccontent/browser/media/android/browser_gpu_video_accelerator_factories.cccontent/browser/renderer_host/compositor_impl_android.cccontent/renderer/render_thread_impl.cccontent/renderer/renderer_blink_platform_impl.cccontent/test/gpu_browsertest_helpers.ccgpu/ipc/common/gpu_channel.mojomgpu/ipc/gl_in_process_context.ccgpu/ipc/in_process_command_buffer.ccgpu/ipc/raster_in_process_context.ccgpu/ipc/service/gles2_command_buffer_stub.ccgpu/ipc/service/raster_command_buffer_stub.ccservices/viz/public/cpp/gpu/context_provider_command_buffer.ccservices/viz/public/cpp/gpu/context_provider_command_buffer.hthird_party/blink/renderer/platform/widget/widget_base.cc
Audit Directions
- Client-controlled service policyIn 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 pathsIn
gles2andrasterdecoder 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 sitesWhere 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.
Patch
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(
Regression Test / PoC
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(
Original Bug Report
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.
- A compromised renderer process creates a new GLES context, setting
lose_context_when_out_of_memory = falsevia theContextCreationAttribsIPC payload. - The attacker allocates a buffer and maps it for writing using
glMapBufferRange. Chrome safely stores the driver’s mapping pointer inmapped_buffer_map. - The attacker issues a
glBufferDatacommand for the exact same buffer with a massivesizeand anullptrdata argument. - 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_MEMORYerror. - Chrome intercepts the error. Because context loss on OOM was disabled in Step 1, the context safely survives.
DoBufferDataearly-returns, abandoning the cleanup and leaving the stale driver pointer inmapped_buffer_map. - The attacker populates the shared memory associated with the mapping with a malicious payload.
- The attacker calls
glUnmapBuffer(orglFlushMappedBufferRange). Chrome retrieves the stale pointer and performs amemcpyof 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.