CVE-2026-17701
Overview
Background
- `EGL_EXT_create_context_robustness`
- an EGL extension that lets an app request a “robust access” context in which out-of-bounds buffer/index accesses must be handled safely (silently clamped/ignored or signaled via context loss) rather than reading arbitrary memory.
- `GL_KHR_robust_buffer_access_behavior`
- the companion GL behavior that guarantees out-of-bounds buffer reads within a draw stay inside allocated storage; the Metal backend does not implement it.
- Element (index) array OOB
- a
glDrawElementscall whose index buffer contains a value larger than the number of vertices supplied, so the pipeline is asked to fetch a vertex beyond the bound vertex buffer. - `DisplayMtl::generateExtensions`
- the Metal backend routine that advertises which EGL display extensions ANGLE exposes to applications on Apple GPUs.
Root Cause Analysis
Before the change the Metal (DisplayMtl) backend never advertised createContextRobustness, so an application on Apple GPUs could not create an EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT context and the front-end robustness path that bounds-checks element-array indices was never engaged for Metal. The advisory frames the fix as enabling EGL_EXT_create_context_robustness, and the patch does so by setting outExtensions->createContextRobustness = true in DisplayMtl::generateExtensions, which turns on the ANGLE front-end’s existing index-range validation so that out-of-bounds element indices are rejected before reaching the GPU. The commit message states that element-array OOB indices are “already prevented by the front-end as long as a context is created with EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT,” meaning the missing capability advertisement, not a new sink, is what left the OOB read path reachable.
The fix works because once robustness is offered and requested, the front-end throws GL_INVALID_OPERATION on the offending glDrawElements (Metal still lacks GL_KHR_robust_buffer_access_behavior, so it cannot silently clamp), preventing the vertex fetch from ranging past the bound buffer. New tests (DrawElementsWithOutOfBoundsIndex) and instantiations for ES2_METAL()/ES3_METAL() lock in that an OOB index no longer crashes or reads out of bounds under a robust context.
createContextRobustness, leaving ANGLE’s front-end OOB index validation inactive for Apple GPUs; the fix simply enables the extension so that path always guards glDrawElements.Attack Path
- Reach a Metal-backed ANGLE context Content running through a GPU-accelerated WebGL/graphics path is serviced by ANGLE’s Metal backend on an Apple GPU.
- Issue an indexed draw with an out-of-range index
The client uploads a small vertex buffer and an element array containing an index beyond the vertex count (e.g. index
100against only 3 vertices), then callsglDrawElements. - OOB vertex fetch attempted
Without robustness advertised, the pipeline is driven to fetch a vertex past the bound buffer, producing an out-of-bounds read in the Metal draw path.
(inferred)— the diff shows only the enablement and tests, not the specific sink that reads past the buffer. - Information disclosure or crash
The out-of-bounds read could expose adjacent GPU/process memory or destabilize the driver; after the fix the front-end instead throws
GL_INVALID_OPERATIONand the draw is rejected.(inferred)beyond the OOB-read classification the advisory states.
Impact Assessment
(inferred) and not demonstrated by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.Files Changed
src/libANGLE/renderer/metal/DisplayMtl.mmsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/deqp_support/deqp_egl_test_expectations.txtsrc/tests/egl_tests/EGLRobustnessTest.cpp
Audit Directions
- Extension capability parityAudit each ANGLE backend’s
generateExtensionsfor security-relevant capabilities (robustness, bounds behaviors) that a peer backend advertises but this one silently omits, since a missing flag can disable front-end safety checks. - Robustness-gated validationLook for validation paths (index-range and buffer-bounds checks) that only activate when a context is robust, and verify every backend can actually create a robust context so the checks are not dead code.
- Indexed-draw OOB sinks on MetalReview
glDrawElements/instanced and indirect draw paths in the Metal backend for out-of-bounds vertex/index fetches that rely on front-end guarding rather than in-backend clamping, especially sinceGL_KHR_robust_buffer_access_behavioris unimplemented there.
Patch
From bf459445a71f5a0a33f5b20289169cda3ef4333e Mon Sep 17 00:00:00 2001 From: Le Hoang Quyen <[email protected]> Date: Thu, 04 Jun 2026 19:33:11 +0800 Subject: [PATCH] Metal: enable EGL_EXT_create_context_robustness Element array's OOB indices are already prevented by the front-end as long as a context is created with EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT attrib. The only incomplete implementation of this extension is that the extension expects OOB access in buffers to execute silently or trigger context loss. However, because the Metal backend doesn't implement GL_KHR_robust_buffer_access_behavior, the front-end will simply throw GL_INVALID_OPERATION in those cases. This is reflected in these dEQP tests: - EGL.functional.robustness.reset_context.fixed_function_pipeline.\ reset_status.index_buffer_out_of_bounds - EGL.functional.robustness.reset_context.fixed_function_pipeline.\ reset_status.vertex_buffer_out_of_bounds Bug: chromium:517972648 Change-Id: Ia7b6e07d3f8b34f7a1669c5e576f168ad6deb159 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7901455 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Quyen Le <[email protected]> --- diff --git a/src/libANGLE/renderer/metal/DisplayMtl.mm b/src/libANGLE/renderer/metal/DisplayMtl.mm index 066733a..da5beda 100644 --- a/src/libANGLE/renderer/metal/DisplayMtl.mm +++ b/src/libANGLE/renderer/metal/DisplayMtl.mm @@ -468,6 +468,7 @@ void DisplayMtl::generateExtensions(egl::DisplayExtensions *outExtensions) const { + outExtensions->createContextRobustness = true; outExtensions->iosurfaceClientBuffer = true; outExtensions->surfacelessContext = true; outExtensions->noConfigContext = true; diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index aeb808a..3454da1 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -2820,6 +2820,9 @@ // Crashing on Win and Linux Vulkan 519817006 VULKAN : VulkanPerformanceCounterTest_TileMemory.RedefineSharedDepthTextureWithOpenRenderPasses/* = SKIP +// This test fails because of incorrect GLX MakeCurrent calls in other tests +520248049 LINUX OPENGL : EGLSurfacelessContextTest.CheckFramebufferStatus/* = SKIP + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Slow tests, should appear last in this file // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/src/tests/deqp_support/deqp_egl_test_expectations.txt b/src/tests/deqp_support/deqp_egl_test_expectations.txt index 1a520f4..4605aa7 100644 --- a/src/tests/deqp_support/deqp_egl_test_expectations.txt +++ b/src/tests/deqp_support/deqp_egl_test_expectations.txt @@ -293,6 +293,10 @@ // New crash since VK-GL-CTS roller on 2023-05-03 42266606 MAC METAL : dEQP-EGL.functional.fence_sync.valid.egl_fence_persistent_buffer = SKIP +// On Metal, these OOB draws will simply throw GL_INVALID_OPERATION instead of causing context loss or being ignored silently +519942256 METAL : dEQP-EGL.functional.robustness.reset_context.fixed_function_pipeline.reset_status.index_buffer_out_of_bounds = SKIP +519942256 METAL : dEQP-EGL.functional.robustness.reset_context.fixed_function_pipeline.reset_status.vertex_buffer_out_of_bounds = SKIP + 42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.get_frame_timestamps.* = FAIL 42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.wide_color.* = FAIL 42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.choose_config.simple.selection_only.transparent_red_value = FAIL diff --git a/src/tests/egl_tests/EGLRobustnessTest.cpp b/src/tests/egl_tests/EGLRobustnessTest.cpp index 6c7a789..3645eb5 100644 --- a/src/tests/egl_tests/EGLRobustnessTest.cpp +++ b/src/tests/egl_tests/EGLRobustnessTest.cpp @@ -865,6 +865,44 @@ } } +// Test drawing with an out-of-bounds index in an index buffer. +// Under robust access, we only expect that the draw call doesn't crash. +TEST_P(EGLRobustnessTestES3, DrawElementsWithOutOfBoundsIndex) +{ + ANGLE_SKIP_TEST_IF(!mInitialized); + + ANGLE_SKIP_TEST_IF( + !IsEGLDisplayExtensionEnabled(mDisplay, "EGL_KHR_create_context") || + !IsEGLDisplayExtensionEnabled(mDisplay, "EGL_EXT_create_context_robustness")); + + createRobustContext(EGL_NO_RESET_NOTIFICATION_EXT, EGL_NO_CONTEXT); + + ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red()); + glUseProgram(program); + + // Provide only 3 vertices (indices 0, 1, 2) + const GLfloat vertices[] = { + -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + }; + GLBuffer vertexBuffer; + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + GLint posLocation = glGetAttribLocation(program, essl3_shaders::PositionAttrib()); + ASSERT_NE(-1, posLocation); + glVertexAttribPointer(posLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(posLocation); + + // Index buffer containing an OOB index (e.g. 100) + const GLushort indices[] = {0, 1, 100}; + GLBuffer indexBuffer; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // Draw. Under robustness, this must not crash the GPU/driver. + glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, nullptr); +} + // Test context destruction after recovering from a long running task. TEST_P(EGLRobustnessTest, DISABLED_LongRunningTaskVulkanShutdown) { @@ -880,6 +918,7 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLRobustnessTestES31); ANGLE_INSTANTIATE_TEST(EGLRobustnessTest, WithNoFixture(ES2_VULKAN()), + WithNoFixture(ES2_METAL()), WithNoFixture(ES2_D3D9()), WithNoFixture(ES2_D3D11()), WithNoFixture(ES2_OPENGL()), @@ -887,6 +926,7 @@ WithNoFixture(ES2_VULKAN_SWIFTSHADER())); ANGLE_INSTANTIATE_TEST(EGLRobustnessTestES3, WithNoFixture(ES3_VULKAN()), + WithNoFixture(ES3_METAL()), WithNoFixture(ES3_D3D11()), WithNoFixture(ES3_OPENGL()), WithNoFixture(ES3_OPENGLES()),
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index aeb808a..3454da1 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2820,6 +2820,9 @@
// Crashing on Win and Linux Vulkan
519817006 VULKAN : VulkanPerformanceCounterTest_TileMemory.RedefineSharedDepthTextureWithOpenRenderPasses/* = SKIP
+// This test fails because of incorrect GLX MakeCurrent calls in other tests
+520248049 LINUX OPENGL : EGLSurfacelessContextTest.CheckFramebufferStatus/* = SKIP
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Slow tests, should appear last in this file
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/src/tests/deqp_support/deqp_egl_test_expectations.txt b/src/tests/deqp_support/deqp_egl_test_expectations.txt
index 1a520f4..4605aa7 100644
--- a/src/tests/deqp_support/deqp_egl_test_expectations.txt
+++ b/src/tests/deqp_support/deqp_egl_test_expectations.txt
@@ -293,6 +293,10 @@
// New crash since VK-GL-CTS roller on 2023-05-03
42266606 MAC METAL : dEQP-EGL.functional.fence_sync.valid.egl_fence_persistent_buffer = SKIP
+// On Metal, these OOB draws will simply throw GL_INVALID_OPERATION instead of causing context loss or being ignored silently
+519942256 METAL : dEQP-EGL.functional.robustness.reset_context.fixed_function_pipeline.reset_status.index_buffer_out_of_bounds = SKIP
+519942256 METAL : dEQP-EGL.functional.robustness.reset_context.fixed_function_pipeline.reset_status.vertex_buffer_out_of_bounds = SKIP
+
42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.get_frame_timestamps.* = FAIL
42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.wide_color.* = FAIL
42265027 PIXEL4ORXL GLES : dEQP-EGL.functional.choose_config.simple.selection_only.transparent_red_value = FAIL
diff --git a/src/tests/egl_tests/EGLRobustnessTest.cpp b/src/tests/egl_tests/EGLRobustnessTest.cpp
index 6c7a789..3645eb5 100644
--- a/src/tests/egl_tests/EGLRobustnessTest.cpp
+++ b/src/tests/egl_tests/EGLRobustnessTest.cpp
@@ -865,6 +865,44 @@
}
}
+// Test drawing with an out-of-bounds index in an index buffer.
+// Under robust access, we only expect that the draw call doesn't crash.
+TEST_P(EGLRobustnessTestES3, DrawElementsWithOutOfBoundsIndex)
+{
+ ANGLE_SKIP_TEST_IF(!mInitialized);
+
+ ANGLE_SKIP_TEST_IF(
+ !IsEGLDisplayExtensionEnabled(mDisplay, "EGL_KHR_create_context") ||
+ !IsEGLDisplayExtensionEnabled(mDisplay, "EGL_EXT_create_context_robustness"));
+
+ createRobustContext(EGL_NO_RESET_NOTIFICATION_EXT, EGL_NO_CONTEXT);
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red());
+ glUseProgram(program);
+
+ // Provide only 3 vertices (indices 0, 1, 2)
+ const GLfloat vertices[] = {
+ -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
+ };
+ GLBuffer vertexBuffer;
+ glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+
+ GLint posLocation = glGetAttribLocation(program, essl3_shaders::PositionAttrib());
+ ASSERT_NE(-1, posLocation);
+ glVertexAttribPointer(posLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+ glEnableVertexAttribArray(posLocation);
+
+ // Index buffer containing an OOB index (e.g. 100)
+ const GLushort indices[] = {0, 1, 100};
+ GLBuffer indexBuffer;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
+
+ // Draw. Under robustness, this must not crash the GPU/driver.
+ glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, nullptr);
+}
+
// Test context destruction after recovering from a long running task.
TEST_P(EGLRobustnessTest, DISABLED_LongRunningTaskVulkanShutdown)
{
@@ -880,6 +918,7 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EGLRobustnessTestES31);
ANGLE_INSTANTIATE_TEST(EGLRobustnessTest,
WithNoFixture(ES2_VULKAN()),
+ WithNoFixture(ES2_METAL()),
WithNoFixture(ES2_D3D9()),
WithNoFixture(ES2_D3D11()),
WithNoFixture(ES2_OPENGL()),
@@ -887,6 +926,7 @@
WithNoFixture(ES2_VULKAN_SWIFTSHADER()));
ANGLE_INSTANTIATE_TEST(EGLRobustnessTestES3,
WithNoFixture(ES3_VULKAN()),
+ WithNoFixture(ES3_METAL()),
WithNoFixture(ES3_D3D11()),
WithNoFixture(ES3_OPENGL()),
WithNoFixture(ES3_OPENGLES()),
Original Bug Report
Potential GPU process OOB buffer read in ANGLE/Metal due to missing context robustness on macOS
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential out-of-bounds GPU buffer read vulnerability exists in the ANGLE Metal backend on macOS. This issue is caused by the silent omission of the EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT attribute when Chrome creates GL contexts because the Metal backend does not advertise EGL_EXT_create_context_robustness. A compromised renderer process could potentially bypass all draw-call bounds validation under the passthrough command decoder, leading to cross-origin GPU data exposure.
Affected files:
third_party/angle/src/libANGLE/renderer/metal/DisplayMtl.mmui/gl/gl_context_egl.ccthird_party/angle/src/libANGLE/Context.cppthird_party/angle/src/libANGLE/validationES.hgpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc
Estimated timestamp from git blame: 2024-01-25
Description
A compromised renderer process can potentially bypass draw-call bounds validation under the OpenGL ES 2.0 passthrough decoder on macOS. This behavior arises from a silent fallback during EGL context initialization where robust buffer access is omitted because ANGLE’s Metal display backend does not advertise EGL_EXT_create_context_robustness support.
Vulnerability Mechanics & Root Cause
-
Context Creation & Mojo Deserialization: A compromised renderer can request a GLES2 command buffer by calling
mojom::GpuChannel::CreateCommandBufferwithattribs.context_typeset toContextType::kOpenGLES2(which is permitted and successfully deserialized bycontext_type_mojom_traits.h). Because the requested context type is not WebGL,webgl_compatibility_contextevaluates tofalseinGenerateGLContextAttribsForDecoder(gpu/command_buffer/service/service_utils.cc). -
Omission of Robust Buffer Access: Inside
GLContextEGL::InitializeImpl(ui/gl/gl_context_egl.cc), context attributes are only populated withEGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXTif the display advertisesb_EGL_EXT_create_context_robustness. On macOS with the default ANGLE Metal backend,DisplayMtl::generateExtensions(third_party/angle/src/libANGLE/renderer/metal/DisplayMtl.mm) does not definecreateContextRobustness. As a result, the robust buffer access attribute is silently dropped during context creation. -
Bypassing Validation in ANGLE: The created ANGLE context has both
mState.isWebGL()andmState.hasRobustAccess()set tofalse. Inside ANGLE’sContext::init(third_party/angle/src/libANGLE/Context.cpp), this causes bothmRequiresRobustBehaviorandmBufferAccessValidationEnabledto resolve tofalse. Consequently, validation entry points (such asValidateDrawArraysAttribsinthird_party/angle/src/libANGLE/validationES.h) immediately returntruewithout verifying vertex array and index bounds against bound buffer sizes. -
Potential GPU Out-Of-Bounds Read: Because the passthrough command decoder forwards drawing operations directly to ANGLE, and ANGLE bypasses CPU-side validation, raw vertex arrays are bound and sent directly to the Metal render command encoder. Since no hardware-level robust access was configured, the GPU vertex fetcher can read out-of-bounds. Given that a global texture and semaphore share group is enabled in the passthrough command decoder, this out-of-bounds read can potentially leak adjacent cross-origin GPU buffer and texture data from other tabs or contexts.
Suggested Potential Reproduction Steps
Note: The following are suggested/potential reproduction steps because our tooling agent does not have the ability to run code or verify a live proof of concept.
- From a compromised renderer on macOS, open a GpuChannel and request a command buffer with
CreateCommandBufferParams.attribsset toContextType::kOpenGLES2(with WebGL compatibility context set to false). - Create and bind a small
GL_ARRAY_BUFFER(e.g., 16 bytes) and configure a vertex attribute pointing to it. - Attach a vertex shader that reads from this attribute and writes the value to a varying or render target.
- Call
glDrawArrayswith a count parameter exceeding the buffer boundary (e.g.,count = 1 << 16). - Retrieve the read data by rendering to a framebuffer and executing
glReadPixelsto inspect potential cross-origin GPU memory content.
Suggested Fix
Support robust context creation in ANGLE’s Metal backend. This can be achieved by implementing robust buffer access behavior or by defining outExtensions->createContextRobustness = true; in DisplayMtl::generateExtensions (third_party/angle/src/libANGLE/renderer/metal/DisplayMtl.mm) and ensuring that appropriate robust emulation or bounds checks are enforced during draw setup if robust access is requested.
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.