Chrome · ANGLE › Metal
CVE-2026-118322
Memory Corruption in ANGLE (Metal)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
AtomicCommandBufferErrorsrc/libANGLE/renderer/metal/mtl_command_buffer.h |
modified |
Files Changed
src/libANGLE/renderer/metal/BufferMtl.mmsrc/libANGLE/renderer/metal/FrameBufferMtl.mmsrc/libANGLE/renderer/metal/ContextMtl.hsrc/libANGLE/renderer/metal/ContextMtl.mmsrc/libANGLE/renderer/metal/mtl_command_buffer.mmsrc/libANGLE/renderer/metal/mtl_command_buffer.h
Patch
From 64b22df8fc3b8aa194e738ad292ea8da14358f67 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Fri, 09 Oct 2026 01:42:05 +0800
Subject: [PATCH] Metal: Handle non-OOM command buffer errors
Previously, ContextMtl::checkCommandBufferError() only raised an error
for MTLCommandBufferErrorOutOfMemory and silently discarded all other
MTLCommandBufferError codes (such as Timeout, PageFault, NotPermitted,
and DeviceRemoved), while ContextMtl::getResetStatus() unconditionally
returned NoError.
- Prevent AtomicCommandBufferError::store() from overwriting non-OOM
GPU fault errors before they are popped.
- Handle non-OOM errors in ContextMtl::checkCommandBufferError() by
setting mContextLost and raising an internal error.
- Return UnknownContextReset from ContextMtl::getResetStatus() when
mContextLost is set.
- Check command buffer errors after GPU synchronization in
FramebufferMtl::readPixelsImpl() and BufferMtl::mapRange().
Bug: chromium:568895029
Change-Id: I0e296243291b1bd698d2bc56ba715cab1c3ee3fa
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8542590
Commit-Queue: Quyen Le <lehoangquyen@google.com>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
---
diff --git a/src/libANGLE/renderer/metal/BufferMtl.mm b/src/libANGLE/renderer/metal/BufferMtl.mm
index ae1f151..7aff37f 100644
--- a/src/libANGLE/renderer/metal/BufferMtl.mm
+++ b/src/libANGLE/renderer/metal/BufferMtl.mm
@@ -246,17 +246,20 @@
if (mapPtr)
{
ContextMtl *contextMtl = mtl::GetImpl(context);
+ uint8_t *ptr = nullptr;
if (mShadowCopy.size() == 0)
{
- *mapPtr = mBuffer
- ->mapWithOpt(contextMtl, (access & GL_MAP_WRITE_BIT) == 0,
- access & GL_MAP_UNSYNCHRONIZED_BIT, offset, length)
- .data();
+ ptr = mBuffer
+ ->mapWithOpt(contextMtl, (access & GL_MAP_WRITE_BIT) == 0,
+ access & GL_MAP_UNSYNCHRONIZED_BIT, offset, length)
+ .data();
}
else
{
- *mapPtr = syncAndObtainShadowCopy(contextMtl, offset, length).data();
+ ptr = syncAndObtainShadowCopy(contextMtl, offset, length).data();
}
+ ANGLE_TRY(contextMtl->checkCommandBufferError());
+ *mapPtr = ptr;
}
return angle::Result::Continue;
diff --git a/src/libANGLE/renderer/metal/ContextMtl.h b/src/libANGLE/renderer/metal/ContextMtl.h
index af77f6e..2159fb2 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.h
+++ b/src/libANGLE/renderer/metal/ContextMtl.h
@@ -347,6 +347,7 @@
void flushCommandBuffer(mtl::CommandBufferFinishOperation operation);
void present(const gl::Context *context, id<CAMetalDrawable> presentationDrawable);
angle::Result finishCommandBuffer();
+ angle::Result checkCommandBufferError();
void addCommandBufferScheduledCallback(std::function<void()> callback);
@@ -531,8 +532,6 @@
angle::Result startOcclusionQueryInRenderPass(QueryMtl *query, bool clearOldValue);
- angle::Result checkCommandBufferError();
-
// Dirty bits.
enum DirtyBitType : size_t
{
@@ -649,6 +648,8 @@
ProvokingVertexHelper mProvokingVertexHelper;
mtl::ContextDevice mContextDevice;
+
+ bool mContextLost = false;
};
} // namespace rx
diff --git a/src/libANGLE/renderer/metal/ContextMtl.mm b/src/libANGLE/renderer/metal/ContextMtl.mm
index 53231b2..c485753 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.mm
+++ b/src/libANGLE/renderer/metal/ContextMtl.mm
@@ -1060,6 +1060,10 @@
// Device loss
gl::GraphicsResetStatus ContextMtl::getResetStatus()
{
+ if (mContextLost)
+ {
+ return gl::GraphicsResetStatus::UnknownContextReset;
+ }
return gl::GraphicsResetStatus::NoError;
}
@@ -2937,9 +2941,19 @@
angle::Result ContextMtl::checkCommandBufferError()
{
- ANGLE_CHECK_GL_ALLOC(
- this, mCmdBuffer.cmdQueue().popCmdBufferError() != MTLCommandBufferErrorOutOfMemory);
- return angle::Result::Continue;
+ switch (mCmdBuffer.cmdQueue().popCmdBufferError())
+ {
+ case MTLCommandBufferErrorNone:
+ return angle::Result::Continue;
+ case MTLCommandBufferErrorOutOfMemory:
+ ANGLE_CHECK_GL_ALLOC(this, false);
+ return angle::Result::Stop;
+ default:
+ mContextLost = true;
+ ANGLE_CHECK(this, false, "Metal command buffer execution failed.",
+ GL_INVALID_OPERATION);
+ return angle::Result::Stop;
+ }
}
} // namespace rx
diff --git a/src/libANGLE/renderer/metal/FrameBufferMtl.mm b/src/libANGLE/renderer/metal/FrameBufferMtl.mm
index 79033ba..5489f53 100644
--- a/src/libANGLE/renderer/metal/FrameBufferMtl.mm
+++ b/src/libANGLE/renderer/metal/FrameBufferMtl.mm
@@ -1698,6 +1698,9 @@
buffer->syncContent(contextMtl, contextMtl->getBlitCommandEncoder());
const uint8_t *bufferData = buffer->mapReadOnly(contextMtl).data();
+ // Verify that the command buffer (including any pending clears/draws and the readback
+ // blit) executed without GPU faults before reading back the buffer contents.
+ ANGLE_TRY(contextMtl->checkCommandBufferError());
angle::Result result = readPixelsCopyImpl(
context, area, packPixelsParams, renderTarget,
@@ -1717,6 +1720,8 @@
{
contextMtl->flushCommandBuffer(mtl::WaitUntilFinished);
}
+ // Verify that prior command buffers affecting this texture completed without GPU faults.
+ ANGLE_TRY(contextMtl->checkCommandBufferError());
angle::MemoryBuffer readPixelRowBuffer;
auto checkedRowPitch = angle::CheckedNumeric<size_t>(area.width) * readAngleFormat.pixelBytes;
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.h b/src/libANGLE/renderer/metal/mtl_command_buffer.h
index 2221ba0..9da3d60 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.h
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.h
@@ -59,7 +59,7 @@
class AtomicCommandBufferError : angle::NonCopyable
{
public:
- void store(MTLCommandBufferError value) { mValue.store(value, std::memory_order_release); }
+ void store(MTLCommandBufferError value);
MTLCommandBufferError pop()
{
return mValue.exchange(MTLCommandBufferErrorNone, std::memory_order_acq_rel);
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.mm b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
index 8f3861e..bd09d5c 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.mm
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
@@ -481,6 +481,21 @@
}
}
+// AtomicCommandBufferError implementation
+void AtomicCommandBufferError::store(MTLCommandBufferError value)
+{
+ // Do not overwrite an existing non-OOM GPU fault error (e.g. Timeout, PageFault,
+ // DeviceRemoved) with a subsequent error (such as NotPermitted from rejected follow-up
+ // submissions or OutOfMemory) so the root-cause fault is preserved until popped.
+ MTLCommandBufferError prevError = mValue.load(std::memory_order_relaxed);
+ while (
+ (prevError == MTLCommandBufferErrorNone || prevError == MTLCommandBufferErrorOutOfMemory) &&
+ !mValue.compare_exchange_weak(prevError, value, std::memory_order_release,
+ std::memory_order_consume))
+ {
+ }
+}
+
// CommandQueue implementation
void CommandQueue::reset()
{
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page