Medium watch CVSS 9.6 chrome Input Validation 🔧 Commit mapped

Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in ANGLE
DescriptionImproper input validation in ANGLE
ComponentANGLE
Sub-componentMetal
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker536450979
Fix commit2c5c60cd270d (angle/angle) +205/-38
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-19 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-15
Patch gap47 days — fix public in source before the advisory

Background

Occlusion query
a GL query object that counts whether/how many fragments pass the depth/stencil test, implemented on Metal via setVisibilityResultMode writing a per-query result into a visibility buffer.
Visibility result offset
the byte offset into the visibility result buffer where a given query’s sample count is stored, passed to Metal for each active query within a render pass encoder.
`mMaxVisibilityQueryOffset`
the device-dependent maximum visibility offset Metal accepts inside one render pass (262136 on Apple GPU family 7, otherwise 65528).
Render pass encoder
a MTLRenderCommandEncoder (mRenderEncoder) whose visibility result buffer and offsets are fixed for the life of the encoding, so exceeding the offset limit requires ending and restarting encoding.

Root Cause Analysis

When a long-lived occlusion query spanned enough render passes, ContextMtl kept allocating fresh visibility result offsets from mtl::OcclusionQueryPool and feeding them to mRenderEncoder via startOcclusionQueryInRenderPass without ever checking them against the Metal device’s maximum supported visibility offset. The invariant that the offset handed to setVisibilityResultMode stays within mMaxVisibilityQueryOffset was never enforced, so a sufficiently large allocated offset became invalid input to the Metal driver.

The fix introduces mtl::OcclusionQueryPool::canAllocateQueryOffset, and both when a query begins and when the querying process resumes in the dirty-render-pass path it now calls endEncoding(true) before crossing the limit, forcing a new render pass encoder whose offsets restart from a valid range. The obsolete restartActiveOcclusionQueryInRenderPass helper is removed because restart is now driven from setupDraw gated on getVisibilityResultMode() == MTLVisibilityResultModeDisabled plus the new offset check.

Key insight
The single mistake was allocating and submitting a visibility result offset without validating it against the device’s per-render-pass maximum; the fix adds an explicit canAllocateQueryOffset bound check that splits the render pass whenever the next offset would exceed mMaxVisibilityQueryOffset.

Attack Path

  1. Begin a long occlusion query Web content (WebGL/WebGL2) starts an occlusion/ANY_SAMPLES_PASSED query that must remain active across many draw calls and render-pass transitions.
  2. Force many render-pass allocations By repeatedly triggering render-pass breaks while the query stays active, the attacker drives mtl::OcclusionQueryPool to allocate an ever-growing sequence of visibility result offsets.
  3. Cross the device offset limit Without the check, an allocated offset eventually exceeds mMaxVisibilityQueryOffset and is passed to setVisibilityResultMode on the render encoder, violating a Metal input constraint.
  4. Trigger the improper-input condition The out-of-range offset reaches the Metal driver as invalid input, corrupting occlusion-query bookkeeping or causing a driver-level fault. (inferred)
  5. Escalate from the malformed GPU submission Any memory-safety consequence in the GPU process or Metal driver beyond the rejected/invalid submission is not demonstrated by the diff. (inferred)

Impact Assessment

An attacker running untrusted WebGL content can submit an occlusion query whose visibility offset exceeds Metal’s per-render-pass maximum, feeding invalid input to the Metal backend in the GPU process; the precondition is a query kept active long enough to accumulate offsets past 65528/262136 bytes. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. Any escalation to memory corruption, information disclosure, or code execution in the GPU process is beyond what the patch demonstrates and is (inferred).

Files Changed

  • util/autogen/angle_features_autogen.h
  • src/libANGLE/renderer/metal/DisplayMtl.mm
  • util/autogen/angle_features_autogen.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
  • src/libANGLE/renderer/metal/ContextMtl.mm
  • src/libANGLE/renderer/metal/mtl_command_buffer.mm
  • src/libANGLE/renderer/metal/DisplayMtl.h
  • src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm
  • include/platform/autogen/FeaturesMtl_autogen.h
  • src/libANGLE/renderer/metal/mtl_render_utils.mm
  • src/libANGLE/renderer/metal/ContextMtl.h
  • src/tests/gl_tests/OcclusionQueriesTest.cpp
  • src/libANGLE/renderer/metal/mtl_command_buffer.h
  • include/platform/mtl_features.json

Audit Directions

  • Unvalidated device-limit offsets
    Audit every setVisibilityResultMode, buffer-offset, and index/instance base passed to Metal encoders to confirm each is bounded against its MTLDevice/DisplayMtl capability before submission.
  • State spanning encoder boundaries
    Review query/transform-feedback/state that survives across render-pass restarts in ContextMtl, since accumulated allocations (like visibility offsets) can silently outgrow single-pass limits.
  • Removed restart helpers
    Look for other in-place “restart”/“resume” paths that reallocate GPU resources without re-checking capacity limits, the same pattern that made restartActiveOcclusionQueryInRenderPass unsafe.
From 2c5c60cd270d1596fa8abe06bd277983852f4b2b Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Wed, 22 Jul 2026 19:13:16 +0800
Subject: [PATCH] Metal: Split render pass on occlusion query offset limit

When an occlusion query's visibility offset exceeds the maximum limit
supported by the Metal device in a single render pass, end the active
render pass encoder and restart the querying process in a new render
pass encoder.

Bug: chromium:536450979
Change-Id: If625475852239174105bcca6759b722de3423f13
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8134016
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
---
diff --git a/include/platform/mtl_features.json b/include/platform/mtl_features.json
index 78f0cbc..acdbfe0 100644
--- a/include/platform/mtl_features.json
+++ b/include/platform/mtl_features.json
@@ -418,6 +418,14 @@
             "description": [
                 "Clear allocated textures to non-zero values to help detect uninitialized reads."
             ]
+        },
+        {
+            "name": "limit_max_visibility_query_offset",
+            "category": "Features",
+            "description": [
+                "Limit maximum visibility query offset to 65528 bytes."
+            ],
+            "issue": "http://anglebug.com/537661068"
         }
     ]
 }
diff --git a/src/libANGLE/renderer/metal/ContextMtl.h b/src/libANGLE/renderer/metal/ContextMtl.h
index bd10f2f..4ea4a3c 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.h
+++ b/src/libANGLE/renderer/metal/ContextMtl.h
@@ -304,10 +304,6 @@
     // Disable the occlusion query in the current render pass.
     // The render pass must already started.
     void disableActiveOcclusionQueryInRenderPass();
-    // Re-enable the occlusion query in the current render pass.
-    // The render pass must already started.
-    // NOTE: the old query's result will be retained and combined with the new result.
-    angle::Result restartActiveOcclusionQueryInRenderPass();
 
     // Invoke by TransformFeedbackMtl
     void onTransformFeedbackActive(const gl::Context *context, TransformFeedbackMtl *xfb);
diff --git a/src/libANGLE/renderer/metal/ContextMtl.mm b/src/libANGLE/renderer/metal/ContextMtl.mm
index c57e665..ad2cd40 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.mm
+++ b/src/libANGLE/renderer/metal/ContextMtl.mm
@@ -2221,6 +2221,11 @@
     ASSERT(mOcclusionQuery == nullptr);
     mOcclusionQuery = query;
 
+    if (mRenderEncoder.valid() && !mOcclusionQueryPool.canAllocateQueryOffset(this))
+    {
+        endEncoding(true);
+    }
+
     if (mRenderEncoder.valid())
     {
         // if render pass has started, start the query in the encoder
@@ -2270,16 +2275,6 @@
                                            mOcclusionQuery->getAllocatedVisibilityOffsets().back());
 }
 
-angle::Result ContextMtl::restartActiveOcclusionQueryInRenderPass()
-{
-    if (!mOcclusionQuery || mOcclusionQuery->getAllocatedVisibilityOffsets().empty())
-    {
-        return angle::Result::Continue;
-    }
-
-    return startOcclusionQueryInRenderPass(mOcclusionQuery, false);
-}
-
 angle::Result ContextMtl::startOcclusionQueryInRenderPass(QueryMtl *query, bool clearOldValue)
 {
     ASSERT(mRenderEncoder.valid());
@@ -2504,10 +2499,18 @@
         ANGLE_TRY(handleDirtyRenderPass(context));
     }
 
-    if (mOcclusionQuery && mOcclusionQueryPool.getNumRenderPassAllocatedQueries() == 0)
+    if (mOcclusionQuery &&
+        mRenderEncoder.getVisibilityResultMode() == MTLVisibilityResultModeDisabled)
     {
-        // The occlusion query is still active, and a new render pass has started.
-        // We need to continue the querying process in the new render encoder.
+        // The occlusion query is still active, and a new render pass has started or we have paused
+        // the querying process. We need to continue the querying process.
+        if (!mOcclusionQueryPool.canAllocateQueryOffset(this))
+        {
+            // The render pass visibility query offset limit has been reached. End the current
+            // encoding; the caller will retry with a new render encoder.
+            endEncoding(true);
+            return angle::Result::Continue;
+        }
         ANGLE_TRY(startOcclusionQueryInRenderPass(mOcclusionQuery, false));
     }
 
diff --git a/src/libANGLE/renderer/metal/DisplayMtl.h b/src/libANGLE/renderer/metal/DisplayMtl.h
index 6901918..f5da632 100644
--- a/src/libANGLE/renderer/metal/DisplayMtl.h
+++ b/src/libANGLE/renderer/metal/DisplayMtl.h
@@ -155,6 +155,7 @@
     mtl::StateCache &getStateCache() { return mStateCache; }
     mtl::LibraryCache &getLibraryCache() { return mLibraryCache; }
     uint32_t getMaxColorTargetBits() { return mMaxColorTargetBits; }
+    uint32_t getMaxVisibilityQueryOffset() const { return mMaxVisibilityQueryOffset; }
     bool hasFragmentMemoryBarriers() const { return mHasFragmentMemoryBarriers; }
 
     id<MTLLibrary> getDefaultShadersLib();
@@ -219,6 +220,7 @@
     mutable gl::Limitations mNativeLimitations;
     mutable ShPixelLocalStorageOptions mNativePLSOptions;
     mutable uint32_t mMaxColorTargetBits = 0;
+    mutable uint32_t mMaxVisibilityQueryOffset = 0;
     mutable bool mHasFragmentMemoryBarriers;
 
     angle::FeaturesMtl mFeatures;
diff --git a/src/libANGLE/renderer/metal/DisplayMtl.mm b/src/libANGLE/renderer/metal/DisplayMtl.mm
index 662e1ab..71f0c61 100644
--- a/src/libANGLE/renderer/metal/DisplayMtl.mm
+++ b/src/libANGLE/renderer/metal/DisplayMtl.mm
@@ -798,6 +798,15 @@
         mMaxColorTargetBits = mNativeCaps.maxColorAttachments * 32;
     }
 
+    if (!mFeatures.limitMaxVisibilityQueryOffset.enabled && supportsAppleGPUFamily(7))
+    {
+        mMaxVisibilityQueryOffset = 262136;
+    }
+    else
+    {
+        mMaxVisibilityQueryOffset = 65528;
+    }
+
     // MSAA
     mNativeCaps.maxSamples             = mFormatTable.getMaxSamples();
     mNativeCaps.maxSampleMaskWords     = 1;
@@ -1204,6 +1213,11 @@
     }
 
     ANGLE_FEATURE_CONDITION((&mFeatures), allowGenMultipleMipsPerPass, true);
+
+    // TODO(anglebug.com/537661068): using visibility query offset > 65528 causes bugs in the Apple
+    // Silicon driver.
+    ANGLE_FEATURE_CONDITION((&mFeatures), limitMaxVisibilityQueryOffset, true);
+
     ANGLE_FEATURE_CONDITION((&mFeatures), forceBufferGPUStorage, false);
     ANGLE_FEATURE_CONDITION((&mFeatures), hasExplicitMemBarrier, (isOSX || isCatalyst) && !isARM);
     ANGLE_FEATURE_CONDITION((&mFeatures), hasDepthAutoResolve, supportsEitherGPUFamily(3, 2));
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.h b/src/libANGLE/renderer/metal/mtl_command_buffer.h
index 8c9b9f2..2221ba0 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.h
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.h
@@ -562,6 +562,7 @@
                                                                      uint32_t baseInstance);
 
     RenderCommandEncoder &setVisibilityResultMode(MTLVisibilityResultMode mode, size_t offset);
+    MTLVisibilityResultMode getVisibilityResultMode() const;
 
     RenderCommandEncoder &useResource(const BufferRef &resource,
                                       MTLResourceUsage usage,
diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.mm b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
index feb19f5..1dd725d 100644
--- a/src/libANGLE/renderer/metal/mtl_command_buffer.mm
+++ b/src/libANGLE/renderer/metal/mtl_command_buffer.mm
@@ -2238,6 +2238,11 @@
     return *this;
 }
 
+MTLVisibilityResultMode RenderCommandEncoder::getVisibilityResultMode() const
+{
+    return mStateCache.visibilityResultMode;
+}
+
 RenderCommandEncoder &RenderCommandEncoder::useResource(const BufferRef &resource,
                                                         MTLResourceUsage usage,
                                                         MTLRenderStages stages)
diff --git a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
index 77db2e8..33325e9 100644
--- a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
+++ b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
@@ -39,6 +39,7 @@
     // the render pass or the query that already has an allocated offset.
     // Note: a query might have more than one allocated offset. They will be combined in the final
     // step.
+    bool canAllocateQueryOffset(ContextMtl *contextMtl) const;
     angle::Result allocateQueryOffset(ContextMtl *contextMtl, QueryMtl *query, bool clearOldValue);
     // Deallocate all offsets used for a query.
     void deallocateQueryOffset(ContextMtl *contextMtl, QueryMtl *query);
diff --git a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm
index 40163a1..8b5555d 100644
--- a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm
+++ b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm
@@ -36,10 +36,20 @@
     mAllocatedQueries.clear();
 }
 
+bool OcclusionQueryPool::canAllocateQueryOffset(ContextMtl *contextMtl) const
+{
+    uint32_t currentOffset =
+        static_cast<uint32_t>(mAllocatedQueries.size()) * kOcclusionQueryResultSize;
+    return currentOffset + kOcclusionQueryResultSize <=
+           contextMtl->getDisplay()->getMaxVisibilityQueryOffset();
+}
+
 angle::Result OcclusionQueryPool::allocateQueryOffset(ContextMtl *contextMtl,
                                                       QueryMtl *query,
                                                       bool clearOldValue)
 {
+    CHECK(canAllocateQueryOffset(contextMtl));
+
     // Only query that already has allocated offset or first query of the render pass is allowed to
     // keep old value. Other queries must be reset to zero before counting the samples visibility in
     // draw calls.
diff --git a/src/libANGLE/renderer/metal/mtl_render_utils.mm b/src/libANGLE/renderer/metal/mtl_render_utils.mm
index 6cc0b00..0710ffe 100644
--- a/src/libANGLE/renderer/metal/mtl_render_utils.mm
+++ b/src/libANGLE/renderer/metal/mtl_render_utils.mm
@@ -224,14 +224,12 @@
     angle::FormatID::R32G32B32A32_UINT,
 };
 
-// Class to automatically disable occlusion query upon entering block and re-able it upon
-// exiting block.
+// Class to automatically disable occlusion query upon entering block. The query will be resumed in
+// the next ContextMtl's draw call.
 struct ScopedDisableOcclusionQuery
 {
-    ScopedDisableOcclusionQuery(ContextMtl *contextMtl,
-                                RenderCommandEncoder *encoder,
-                                angle::Result *resultOut)
-        : mContextMtl(contextMtl), mEncoder(encoder), mResultOut(resultOut)
+    ScopedDisableOcclusionQuery(ContextMtl *contextMtl, RenderCommandEncoder *encoder)
+        : mContextMtl(contextMtl), mEncoder(encoder)
     {
 #ifndef NDEBUG
         if (contextMtl->hasActiveOcclusionQuery())
@@ -244,13 +242,13 @@
     }
     ~ScopedDisableOcclusionQuery()
     {
-        *mResultOut = mContextMtl->restartActiveOcclusionQueryInRenderPass();
 #ifndef NDEBUG
         if (mContextMtl->hasActiveOcclusionQuery())
         {
             mEncoder->popDebugGroup();
         }
 #else
+        ANGLE_UNUSED_VARIABLE(mContextMtl);
         ANGLE_UNUSED_VARIABLE(mEncoder);
 #endif
     }
@@ -258,8 +256,6 @@
   private:
     ContextMtl *mContextMtl;
     RenderCommandEncoder *mEncoder;
-
-    angle::Result *mResultOut;
 };
 
 void GetBlitTexCoords(const NormalizedCoords &normalizedCoords,
@@ -1163,10 +1159,9 @@
     ContextMtl *contextMtl = GetImpl(context);
     ANGLE_TRY(setupClearWithDraw(context, cmdEncoder, overridedParams));
 
-    angle::Result result;
     {
         // Need to disable occlusion query, otherwise clearing will affect the occlusion counting
-        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result);
+        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder);
         // Draw the screen aligned triangle
         cmdEncoder->draw(MTLPrimitiveTypeTriangle, 0, 3);
     }
@@ -1174,7 +1169,7 @@
     // Invalidate current context's state
     contextMtl->invalidateState(context);
 
-    return result;
+    return angle::Result::Continue;
 }
 
 // ColorBlitUtils implementation
@@ -1309,10 +1304,9 @@
     ContextMtl *contextMtl = GetImpl(context);
     ANGLE_TRY(setupColorBlitWithDraw(context, cmdEncoder, params));
 
-    angle::Result result;
     {
         // Need to disable occlusion query, otherwise blitting will affect the occlusion counting
-        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result);
+        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder);
         // Draw the screen aligned quad
         cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4);
     }
@@ -1320,7 +1314,7 @@
     // Invalidate current context's state
     contextMtl->invalidateState(context);
 
-    return result;
+    return angle::Result::Continue;
 }
 
 angle::Result DepthStencilBlitUtils::ensureShadersInitialized(
@@ -1517,10 +1511,9 @@
 
     ANGLE_TRY(setupDepthStencilBlitWithDraw(context, cmdEncoder, params));
 
-    angle::Result result;
     {
         // Need to disable occlusion query, otherwise blitting will affect the occlusion counting
-        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result);
+        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder);
         // Draw the screen aligned quad
         cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4);
     }
@@ -1528,7 +1521,7 @@
     // Invalidate current context's state
     contextMtl->invalidateState(context);
 
-    return result;
+    return angle::Result::Continue;
 }
 
 angle::Result DepthStencilBlitUtils::blitStencilViaCopyBuffer(
@@ -2493,10 +2486,9 @@
     options.textureOffset[1]  = params.textureArea.y;
     cmdEncoder->setFragmentData(options, 0);
 
-    angle::Result result;
     {
         // Need to disable occlusion query, otherwise blitting will affect the occlusion counting
-        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result);
+        ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder);
         // Draw the screen aligned quad
         cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4);
     }
@@ -2504,7 +2496,7 @@
     // Invalidate current context's state
     contextMtl->invalidateState(context);
 
-    return result;
+    return angle::Result::Continue;
 }
 
 angle::Result CopyPixelsUtils::packPixelsCS(ContextMtl *contextMtl,
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 8c536a2..c3095c5 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2850,6 +2850,9 @@
 529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
 529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
 
+// Using many queries will fail on Pixel 4 GLES
+537692502 PIXEL4ORXL GLES : OcclusionQueriesTest.ExcessiveNumberOfQueries/* = SKIP
+
 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 // Slow tests, should appear last in this file
 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/src/tests/gl_tests/OcclusionQueriesTest.cpp b/src/tests/gl_tests/OcclusionQueriesTest.cpp
index f45e80e..429e9da 100644
--- a/src/tests/gl_tests/OcclusionQueriesTest.cpp
+++ b/src/tests/gl_tests/OcclusionQueriesTest.cpp
@@ -1317,6 +1317,130 @@
     EXPECT_GL_TRUE(result);
 }
 
+// Test that a very large number of scissored clears while an occlusion query is active still
+// yields a correct query result. On backends that emulate scissored clears with draws, each clear
+// pauses and resumes the query in the same render pass; the render pass must be split before the
+// backend's per-pass visibility offset limit is reached.
+TEST_P(OcclusionQueriesTest, ManyScissoredClearsInsideQuery)
+{
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+                       !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean"));
+
+    // Metal allows at most 32768 visibility result slots per render pass.
+    constexpr int kClearCount = 35000;
+
+    glDepthMask(GL_TRUE);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+    EXPECT_GL_NO_ERROR();
+
+    GLQueryEXT query;
+    glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, query);
+
+    // this quad should not be occluded
+    drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+
+    glEnable(GL_SCISSOR_TEST);
+    glScissor(0, 0, 1, 1);
+    for (int i = 0; i < kClearCount; ++i)
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 8c536a2..c3095c5 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2850,6 +2850,9 @@
 529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
 529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
 
+// Using many queries will fail on Pixel 4 GLES
+537692502 PIXEL4ORXL GLES : OcclusionQueriesTest.ExcessiveNumberOfQueries/* = SKIP
+
 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 // Slow tests, should appear last in this file
 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/src/tests/gl_tests/OcclusionQueriesTest.cpp b/src/tests/gl_tests/OcclusionQueriesTest.cpp
index f45e80e..429e9da 100644
--- a/src/tests/gl_tests/OcclusionQueriesTest.cpp
+++ b/src/tests/gl_tests/OcclusionQueriesTest.cpp
@@ -1317,6 +1317,130 @@
     EXPECT_GL_TRUE(result);
 }
 
+// Test that a very large number of scissored clears while an occlusion query is active still
+// yields a correct query result. On backends that emulate scissored clears with draws, each clear
+// pauses and resumes the query in the same render pass; the render pass must be split before the
+// backend's per-pass visibility offset limit is reached.
+TEST_P(OcclusionQueriesTest, ManyScissoredClearsInsideQuery)
+{
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+                       !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean"));
+
+    // Metal allows at most 32768 visibility result slots per render pass.
+    constexpr int kClearCount = 35000;
+
+    glDepthMask(GL_TRUE);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+    EXPECT_GL_NO_ERROR();
+
+    GLQueryEXT query;
+    glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, query);
+
+    // this quad should not be occluded
+    drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+
+    glEnable(GL_SCISSOR_TEST);
+    glScissor(0, 0, 1, 1);
+    for (int i = 0; i < kClearCount; ++i)
+    {
+        glClear(GL_COLOR_BUFFER_BIT);
+    }
+    glDisable(GL_SCISSOR_TEST);
+
+    // this quad should not be occluded
+    drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+
+    glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT);
+    EXPECT_GL_NO_ERROR();
+
+    GLuint result = GL_FALSE;
+    glGetQueryObjectuivEXT(query, GL_QUERY_RESULT_EXT,
+                           &result);  // will block waiting for result
+    EXPECT_GL_NO_ERROR();
+
+    EXPECT_GL_TRUE(result);
+}
+
+// Test that a very large number of distinct queries and draw calls in a single render pass
+// still works and yields correct query results when exceeding the backend's per-pass
+// visibility offset limit.
+TEST_P(OcclusionQueriesTest, ExcessiveNumberOfQueries)
+{
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+                       !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean"));
+
+    constexpr int kQueryCount = 35000;
+
+    glDisable(GL_DEPTH_TEST);
+    glClear(GL_COLOR_BUFFER_BIT);
+    EXPECT_GL_NO_ERROR();
+
+    constexpr char kVS[] =
+        "attribute vec4 a_position;\n"
+        "attribute vec4 a_color;\n"
+        "varying vec4 v_color;\n"
+        "void main() {\n"
+        "    gl_Position  = a_position;\n"
+        "    gl_PointSize = 4.0;\n"
+        "    v_color      = a_color;\n"
+        "}\n";
+
+    constexpr char kFS[] =
+        "precision mediump float;\n"
+        "varying vec4 v_color;\n"
+        "void main() {\n"
+        "    gl_FragColor = v_color;\n"
+        "}\n";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    GLint posLoc   = glGetAttribLocation(program, "a_position");
+    GLint colorLoc = glGetAttribLocation(program, "a_color");
+    ASSERT_NE(-1, posLoc);
+    ASSERT_NE(-1, colorLoc);
+
+    glDisableVertexAttribArray(posLoc);
+    glDisableVertexAttribArray(colorLoc);
+
+    std::vector<GLQueryEXT> queries(kQueryCount);
+
+    for (int i = 0; i < kQueryCount; ++i)
+    {
+        glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, queries[i]);
+        if (i % 2 != 0)
+        {
+            int idx   = i / 2;
+            GLfloat x = (static_cast<GLfloat>(idx % 30) / 30.0f) * 1.8f - 0.9f;
+            GLfloat y = (static_cast<GLfloat>((idx / 30) % 30) / 30.0f) * 1.8f - 0.9f;
+            glVertexAttrib3f(posLoc, x, y, 0.0f);
+
+            GLfloat r = static_cast<GLfloat>(i % 256) / 255.0f;
+            GLfloat g = static_cast<GLfloat>((i * 7) % 256) / 255.0f;
+            GLfloat b = static_cast<GLfloat>((i * 13) % 256) / 255.0f;
+            glVertexAttrib4f(colorLoc, r, g, b, 1.0f);
+            glDrawArrays(GL_POINTS, 0, 1);
+        }
+        glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT);
+    }
+    EXPECT_GL_NO_ERROR();
+
+    for (int i = 0; i < kQueryCount; ++i)
+    {
+        GLuint result = GL_FALSE;
+        glGetQueryObjectuivEXT(queries[i], GL_QUERY_RESULT_EXT, &result);
+        EXPECT_GL_NO_ERROR();
+        if (i % 2 != 0)
+        {
+            EXPECT_GL_TRUE(result) << " at query index " << i;
+        }
+        else
+        {
+            EXPECT_GL_FALSE(result) << " at query index " << i;
+        }
+    }
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(OcclusionQueriesTest);
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OcclusionQueriesTestES3);
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.