CVE-2026-43709
Overview
Background
- ANGLE
- The translation layer WebKit uses to run WebGL/WebGPU on top of native GPU APIs; its Metal backend (ContextMtl/QueryMtl) runs inside the GPU process.
- Occlusion query
- A GPU feature that counts whether fragments passed the depth test; WebGL/WebGPU expose it, and the result is written into a Metal visibility-result buffer.
- Render pass
- A unit of Metal GPU work with a defined begin/end. A query can start in one pass and continue in another, so its lifetime is not naturally tied to a single pass.
Root Cause Analysis
This fixes a use-after-free in the ANGLE Metal backend’s occlusion-query handling (ContextMtl), used by WebGL/WebGPU occlusion queries.
Before the fix, ContextMtl stored a raw pointer to the active query (QueryMtl* mOcclusionQuery) and dereferenced it across render-pass boundaries in begin/end/destroy/restart (e.g. disableActiveOcclusionQueryInRenderPass, restartActiveOcclusionQueryInRenderPass, startOcclusionQueryInRenderPass all read mOcclusionQuery->getAllocatedVisibilityOffsets()). The query object’s lifetime is not tied to the render pass, so with adversarial timing of EndQuery/DeleteQueries versus render-pass start/end, ContextMtl could dereference a QueryMtl that had already been destroyed (or whose visibility offsets were freed), causing a use-after-free / crash in the GPU process.
The fix removes the raw query pointer entirely: onOcclusionQueryBegin now captures the query’s ref-counted visibility result buffer (mtl::BufferRef mOcclusionQueryResultBuffer) and a flag (mOcclusionQueryIsEnabledInRenderPass), and all continue/disable operations work off that buffer plus pool offsets (beginQuery/continueQuery/discardQuery) rather than the QueryMtl object. onOcclusionQueryDestroy takes the query only to discard its buffer via the pool (valid whether or not end was called), QueryMtl::onDestroy guards on mVisibilityResultBuffer, and RenderCommandEncoder checks hasPendingVisibilityResults().
The restored invariant is that ContextMtl never dereferences a QueryMtl that may have been freed — it holds a strong reference to the result buffer for the query’s active duration instead. INFERENCE: this is a broad refactor of the ANGLE Metal occlusion path; the precise freed-pointer dereference is one of the mOcclusionQuery-> uses removed here, and ANGLE-internal details beyond the shown hunks are not all included.
Attack Path
- Use occlusion queries from WebGL/WebGPU Web content issues BeginQuery/EndQuery (ANY_SAMPLES(_CONSERVATIVE)) on the Metal-backed ANGLE context.
- Span render-pass boundaries The query is kept active while a render pass ends and a new one begins, exercising ContextMtl’s continue/disable paths that read the raw mOcclusionQuery pointer.
- Destroy the query with adversarial timing deleteQueries / context teardown frees the QueryMtl (or its visibility offsets) while ContextMtl still holds the raw pointer.
- Use-after-free in the GPU backend ContextMtl dereferences the freed QueryMtl when continuing or disabling the query in a render pass, crashing the process (now avoided by holding the result buffer by reference).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
DisplayMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
FramebufferMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
QueryMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
VertexArrayMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
ProgramMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
ProgramExecutableMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h |
modified | |
ContextMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h |
modified | |
VisibilityBufferOffsetsMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h |
modified | |
QueryMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h |
modified | |
ContextMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h |
modified | |
QueryMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h |
modified | |
VisibilityBufferOffsetsMtlSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h |
modified |
Files Changed
LayoutTests/platform/mac-wk2/TestExpectationsSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.hSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mmSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.hSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.mmSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_command_buffer.mmSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.hSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mmSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_render_utils.hSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_render_utils.mmSource/ThirdParty/ANGLE/src/tests/gl_tests/OcclusionQueriesTest.cpp
Audit Directions
- Other raw GL-object pointers across pass boundariesAudit the ANGLE Metal backend for other renderer objects cached as raw pointers (not RefPtr/BufferRef) and dereferenced after a render-pass boundary or across begin/end/destroy transitions.
- Query/encoder lifetime couplingCheck every place that assumes a query, buffer, or encoder outlives the operation using it; prefer capturing the ref-counted resource over the owning object.
Patch
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h
index 41553c65283a..a85c8ecffa9f 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.h
@@ -31,6 +31,7 @@ namespace rx
{
class DisplayMtl;
class FramebufferMtl;
+class QueryMtl;
class VertexArrayMtl;
class ProgramMtl;
class ProgramExecutableMtl;
@@ -301,20 +302,22 @@ class ContextMtl : public ContextImpl, public mtl::Context
bool renderPassChanged);
void onBackbufferResized(const gl::Context *context, WindowSurfaceMtl *backbuffer);
- // Invoke by QueryMtl
- angle::Result onOcclusionQueryBegin(const gl::Context *context, QueryMtl *query);
- void onOcclusionQueryEnd(const gl::Context *context, QueryMtl *query);
- void onOcclusionQueryDestroy(const gl::Context *context, QueryMtl *query);
+ angle::Result onOcclusionQueryBegin(QueryMtl &query);
+ void onOcclusionQueryEnd();
+ void onOcclusionQueryDestroy(QueryMtl &query);
// Useful for temporarily pause then restart occlusion query during clear/blit with draw.
- bool hasActiveOcclusionQuery() const { return mOcclusionQuery; }
+ bool isOcclusionQueryEnabledInRenderPass() const
+ {
+ return mOcclusionQueryIsEnabledInRenderPass;
+ }
// Disable the occlusion query in the current render pass.
// The render pass must already started.
- void disableActiveOcclusionQueryInRenderPass();
+ void disableOcclusionQueryInRenderPass();
// 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();
+ angle::Result enableOcclusionQueryInRenderPass();
// Invoke by TransformFeedbackMtl
void onTransformFeedbackActive(const gl::Context *context, TransformFeedbackMtl *xfb);
@@ -536,8 +539,6 @@ class ContextMtl : public ContextImpl, public mtl::Context
bool xfbPass,
bool *pipelineDescChanged);
- angle::Result startOcclusionQueryInRenderPass(QueryMtl *query, bool clearOldValue);
-
angle::Result checkCommandBufferError();
// Dirty bits.
@@ -607,7 +608,6 @@ class ContextMtl : public ContextImpl, public mtl::Context
FramebufferMtl *mDrawFramebuffer = nullptr;
VertexArrayMtl *mVertexArray = nullptr;
ProgramExecutableMtl *mExecutable = nullptr;
- QueryMtl *mOcclusionQuery = nullptr;
using DirtyBits = angle::BitSet<DIRTY_BIT_MAX>;
@@ -658,6 +658,9 @@ class ContextMtl : public ContextImpl, public mtl::Context
id<MTLTexture> mRasterizationRateMapTexture;
mtl::ContextDevice mContextDevice;
+
+ mtl::BufferRef mOcclusionQueryResultBuffer;
+ bool mOcclusionQueryIsEnabledInRenderPass{false};
};
} // namespace rx
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mm
index 644b213beee9..8703f6c2d578 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ContextMtl.mm
@@ -1823,7 +1823,7 @@ void ContextMtl::endEncoding(bool forceSaveRenderPassContent)
mRenderEncoder.setStoreAction(MTLStoreActionStore);
}
- disableActiveOcclusionQueryInRenderPass();
+ disableOcclusionQueryInRenderPass();
mOcclusionQueryPool.prepareRenderPassVisibilityPoolBuffer(this);
@@ -2281,84 +2281,67 @@ void ContextMtl::onBackbufferResized(const gl::Context *context, WindowSurfaceMt
onDrawFrameBufferChangedState(context, framebuffer, true);
}
-angle::Result ContextMtl::onOcclusionQueryBegin(const gl::Context *context, QueryMtl *query)
+angle::Result ContextMtl::onOcclusionQueryBegin(QueryMtl &query)
{
- ASSERT(mOcclusionQuery == nullptr);
- mOcclusionQuery = query;
-
+ ASSERT(!mOcclusionQueryResultBuffer); // Frontend guarantees none active at the time.
+ const mtl::BufferRef &resultBuffer = query.getVisibilityResultBuffer();
+ bool isEnabledInRenderPass;
if (mRenderEncoder.valid())
{
- // if render pass has started, start the query in the encoder
- return startOcclusionQueryInRenderPass(query, true);
+ size_t resultOffset;
+ ANGLE_TRY(mOcclusionQueryPool.beginQuery(this, resultBuffer, &resultOffset));
+ mRenderEncoder.setVisibilityResultMode(MTLVisibilityResultModeBoolean, resultOffset);
+ // Result is available after flush.
+ mCmdBuffer.setWriteDependency(resultBuffer, /*isRenderCommand=*/true);
+ isEnabledInRenderPass = true;
}
else
{
- query->resetVisibilityResult(this);
+ // Reset the occlusion query result stored in buffer to zero.
+ // Later draws will use continueQuery() to enable the visibility buffer writes.
+ auto blitEncoder = getBlitCommandEncoder();
+ blitEncoder->fillBuffer(resultBuffer, NSMakeRange(0, mtl::kOcclusionQueryResultSize), 0);
+ resultBuffer->syncContent(this, blitEncoder);
+ isEnabledInRenderPass = false;
}
-
+ mOcclusionQueryResultBuffer = resultBuffer;
+ mOcclusionQueryIsEnabledInRenderPass = isEnabledInRenderPass;
return angle::Result::Continue;
}
-void ContextMtl::onOcclusionQueryEnd(const gl::Context *context, QueryMtl *query)
-{
- ASSERT(mOcclusionQuery == query);
-
- if (mRenderEncoder.valid())
- {
- // if render pass has started, end the query in the encoder
- disableActiveOcclusionQueryInRenderPass();
- }
- mOcclusionQuery = nullptr;
-}
-void ContextMtl::onOcclusionQueryDestroy(const gl::Context *context, QueryMtl *query)
+void ContextMtl::onOcclusionQueryEnd()
{
- if (query->getAllocatedVisibilityOffsets().empty())
- {
- return;
- }
- if (mOcclusionQuery == query)
- {
- onOcclusionQueryEnd(context, query);
- }
- mOcclusionQueryPool.deallocateQueryOffset(this, query);
+ ASSERT(mOcclusionQueryResultBuffer); // Frontend guarantees one active at the time.
+ disableOcclusionQueryInRenderPass();
+ mOcclusionQueryResultBuffer = nullptr;
}
-void ContextMtl::disableActiveOcclusionQueryInRenderPass()
+void ContextMtl::onOcclusionQueryDestroy(QueryMtl &query)
{
- if (!mOcclusionQuery || mOcclusionQuery->getAllocatedVisibilityOffsets().empty())
- {
- return;
- }
-
- ASSERT(mRenderEncoder.valid());
- mRenderEncoder.setVisibilityResultMode(MTLVisibilityResultModeDisabled,
- mOcclusionQuery->getAllocatedVisibilityOffsets().back());
+ // On normal operation frontend guaraantees that end is called before destroy.
+ // On context destruction, active query is destroyed without end.
+ // Discard is valid for both.
+ mOcclusionQueryPool.discardQuery(query.getVisibilityResultBuffer());
}
-angle::Result ContextMtl::restartActiveOcclusionQueryInRenderPass()
+void ContextMtl::disableOcclusionQueryInRenderPass()
{
- if (!mOcclusionQuery || mOcclusionQuery->getAllocatedVisibilityOffsets().empty())
+ if (mOcclusionQueryResultBuffer && mOcclusionQueryIsEnabledInRenderPass)
{
- return angle::Result::Continue;
+ ASSERT(mRenderEncoder.valid());
+ mRenderEncoder.setVisibilityResultMode(MTLVisibilityResultModeDisabled, 0);
+ mOcclusionQueryIsEnabledInRenderPass = false;
}
-
- return startOcclusionQueryInRenderPass(mOcclusionQuery, false);
}
-angle::Result ContextMtl::startOcclusionQueryInRenderPass(QueryMtl *query, bool clearOldValue)
+angle::Result ContextMtl::enableOcclusionQueryInRenderPass()
{
ASSERT(mRenderEncoder.valid());
-
- ANGLE_TRY(mOcclusionQueryPool.allocateQueryOffset(this, query, clearOldValue));
-
- mRenderEncoder.setVisibilityResultMode(MTLVisibilityResultModeBoolean,
- query->getAllocatedVisibilityOffsets().back());
-
- // We need to mark the query's buffer as being written in this command buffer now. Since the
- // actual writing is deferred until the render pass ends and user could try to read the query
- // result before the render pass ends.
- mCmdBuffer.setWriteDependency(query->getVisibilityResultBuffer(), /*isRenderCommand=*/true);
-
+ size_t resultOffset;
+ ANGLE_TRY(mOcclusionQueryPool.continueQuery(this, mOcclusionQueryResultBuffer, &resultOffset));
+ mRenderEncoder.setVisibilityResultMode(MTLVisibilityResultModeBoolean, resultOffset);
+ mCmdBuffer.setWriteDependency(mOcclusionQueryResultBuffer, /*isRenderCommand=*/true);
+ mOcclusionQueryIsEnabledInRenderPass = true;
return angle::Result::Continue;
}
@@ -2586,11 +2569,11 @@ angle::Result ContextMtl::setupDrawImpl(const gl::Context *context,
ANGLE_TRY(handleDirtyRenderPass(context));
}
- if (mOcclusionQuery && mOcclusionQueryPool.getNumRenderPassAllocatedQueries() == 0)
+ if (mOcclusionQueryResultBuffer && !mOcclusionQueryIsEnabledInRenderPass)
{
// 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.
- ANGLE_TRY(startOcclusionQueryInRenderPass(mOcclusionQuery, false));
+ ANGLE_TRY(enableOcclusionQueryInRenderPass());
}
bool isPipelineDescChanged;
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h
index 363d7657a2b9..104bc924afcc 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.h
@@ -19,50 +19,6 @@ namespace rx
class ContextMtl;
-// The class represents offset(s) allocated in the visiblity buffer for an occlusion query.
-// See doc/OcclusionQuery.md.
-// An occlusion query might have more than one offsets allocated, but all of them must be adjacent
-// to each other. Multiple offsets typically allocated when the query is paused and resumed during
-// viewport clear emulation with draw operations. In such case, Metal doesn't allow an offset to
-// be reused in a render pass, hence multiple offsets will be allocated, and their values will
-// be accumulated.
-class VisibilityBufferOffsetsMtl
-{
- public:
- void clear() { mStartOffset = mNumOffsets = 0; }
- bool empty() const { return mNumOffsets == 0; }
- uint32_t size() const { return mNumOffsets; }
-
- // Return last offset
- uint32_t back() const
- {
- ASSERT(!empty());
- return mStartOffset + (mNumOffsets - 1) * mtl::kOcclusionQueryResultSize;
- }
-
- uint32_t front() const
- {
- ASSERT(!empty());
- return mStartOffset;
- }
-
- void setStartOffset(uint32_t offset)
- {
- ASSERT(empty());
- mStartOffset = offset;
- mNumOffsets = 1;
- }
- void addOffset()
- {
- ASSERT(!empty());
- mNumOffsets++;
- }
-
- private:
- uint32_t mStartOffset = 0;
- uint32_t mNumOffsets = 0;
-};
-
class QueryMtl : public QueryImpl
{
public:
@@ -80,24 +36,9 @@ class QueryMtl : public QueryImpl
angle::Result getResult(const gl::Context *context, GLuint64 *params) override;
angle::Result isResultAvailable(const gl::Context *context, bool *available) override;
- // Get allocated offsets in the render pass's occlusion query pool.
- const VisibilityBufferOffsetsMtl &getAllocatedVisibilityOffsets() const
- {
- return mVisibilityBufferOffsets;
- }
- // Set first allocated offset in the render pass's occlusion query pool.
- void setFirstAllocatedVisibilityOffset(uint32_t off)
- {
- mVisibilityBufferOffsets.setStartOffset(off);
- }
- // Add more offset allocated for the occlusion query
- void addAllocatedVisibilityOffset() { mVisibilityBufferOffsets.addOffset(); }
-
- void clearAllocatedVisibilityOffsets() { mVisibilityBufferOffsets.clear(); }
// Returns the buffer containing the final occlusion query result.
const mtl::BufferRef &getVisibilityResultBuffer() const { return mVisibilityResultBuffer; }
- // Reset the occlusion query result stored in buffer to zero
- void resetVisibilityResult(ContextMtl *contextMtl);
+
void onTransformFeedbackEnd(const gl::Context *context);
// If the timestamp query is still active upon context switch,
@@ -110,8 +51,7 @@ class QueryMtl : public QueryImpl
template <typename T>
angle::Result waitAndGetResult(const gl::Context *context, T *params);
- // List of offsets in the render pass's occlusion query pool buffer allocated for this query
- VisibilityBufferOffsetsMtl mVisibilityBufferOffsets;
+ // Buffer holding the final combined visibility result for this query.
mtl::BufferRef mVisibilityResultBuffer;
size_t mTransformFeedbackPrimitivesDrawn = 0;
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.mm
index 14908006ca4e..94ebb5b9827f 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/QueryMtl.mm
@@ -24,9 +24,9 @@ QueryMtl::~QueryMtl() {}
void QueryMtl::onDestroy(const gl::Context *context)
{
ContextMtl *contextMtl = mtl::GetImpl(context);
- if (!getAllocatedVisibilityOffsets().empty())
+ if (mVisibilityResultBuffer)
{
- contextMtl->onOcclusionQueryDestroy(context, this);
+ contextMtl->onOcclusionQueryDestroy(*this);
}
mVisibilityResultBuffer = nullptr;
}
@@ -51,7 +51,7 @@ angle::Result QueryMtl::begin(const gl::Context *context)
}
}
- ANGLE_TRY(contextMtl->onOcclusionQueryBegin(context, this));
+ ANGLE_TRY(contextMtl->onOcclusionQueryBegin(*this));
break;
case gl::QueryType::TransformFeedbackPrimitivesWritten:
mTransformFeedbackPrimitivesDrawn = 0;
@@ -85,7 +85,7 @@ angle::Result QueryMtl::end(const gl::Context *context)
{
case gl::QueryType::AnySamples:
case gl::QueryType::AnySamplesConservative:
- contextMtl->onOcclusionQueryEnd(context, this);
+ contextMtl->onOcclusionQueryEnd();
break;
case gl::QueryType::TransformFeedbackPrimitivesWritten:
onTransformFeedbackEnd(context);
@@ -207,18 +207,6 @@ angle::Result QueryMtl::getResult(const gl::Context *context, GLuint64 *params)
return waitAndGetResult(context, params);
}
-void QueryMtl::resetVisibilityResult(ContextMtl *contextMtl)
-{
- // Occlusion query buffer must be allocated in QueryMtl::begin
- ASSERT(mVisibilityResultBuffer);
-
- // Fill the query's buffer with zeros
- auto blitEncoder = contextMtl->getBlitCommandEncoder();
- blitEncoder->fillBuffer(mVisibilityResultBuffer, NSMakeRange(0, mtl::kOcclusionQueryResultSize),
- 0);
- mVisibilityResultBuffer->syncContent(contextMtl, blitEncoder);
-}
-
void QueryMtl::onTransformFeedbackEnd(const gl::Context *context)
{
gl::TransformFeedback *transformFeedback = context->getState().getCurrentTransformFeedback();
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_command_buffer.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_command_buffer.mm
index 3934b7d2f8ff..1d93726a5702 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_command_buffer.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_command_buffer.mm
@@ -1542,8 +1542,7 @@ void RenderCommandEncoder::endEncodingImpl(bool considerDiscardSimulation)
objCRenderPassDesc.stencilAttachment))
hasAttachment = true;
- // Set visibility result buffer
- if (mOcclusionQueryPool.getNumRenderPassAllocatedQueries())
+ if (mOcclusionQueryPool.hasPendingVisibilityResults())
{
objCRenderPassDesc.visibilityResultBuffer =
mOcclusionQueryPool.getRenderPassVisibilityPoolBuffer()->get();
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
index 77db2e871c48..e7dae17b2dc1 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h
@@ -20,11 +20,37 @@ namespace rx
{
class ContextMtl;
-class QueryMtl;
namespace mtl
{
+// Tracks allocations of visibility buffer offsets for a query within the render pass.
+// An occlusion query might have more than one offsets allocated, but all of them must be adjacent
+// to each other. Multiple offsets typically allocated when the query is paused and resumed during
+// viewport clear emulation with draw operations. In such case, Metal doesn't allow an offset to
+// be reused in a render pass, hence multiple offsets will be allocated, and their values will
+// be accumulated.
+class VisibilityBufferOffsetsMtl
+{
+ public:
+ VisibilityBufferOffsetsMtl(const BufferRef &buffer, size_t startOffset, size_t size)
... (file diff truncated)
Regression Test / PoC
diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations
index 5661950f388b..5066d4b54544 100644
--- a/LayoutTests/platform/mac-wk2/TestExpectations
+++ b/LayoutTests/platform/mac-wk2/TestExpectations
@@ -2309,6 +2309,8 @@ webkit.org/b/315877 [ Sequoia Debug x86_64 ] webgl/2.0.y/conformance2/textures/m
webkit.org/b/315877 [ Sequoia Debug x86_64 ] webgl/2.0.y/conformance2/textures/webgl_canvas/tex-3d-r11f_g11f_b10f-rgb-float.html [ Pass Timeout ]
webkit.org/b/315877 [ Sequoia Debug x86_64 ] webgl/2.0.y/conformance2/textures/image_bitmap_from_video/tex-2d-rgba32f-rgba-float.html [ Pass Failure ]
+webkit.org/b/318751 [ Debug arm64 ] webgl/2.0.y/conformance2/textures/misc/tex-unpack-params.html [ Pass Failure ]
+
webkit.org/b/315881 [ Sequoia Release x86_64 ] fast/attachment/cocoa/wide-attachment-class.html [ Pass ImageOnlyFailure ]
webkit.org/b/315881 [ Sequoia Release x86_64 ] fast/attachment/cocoa/wide-attachment-default-icon.html [ Pass ImageOnlyFailure ]
webkit.org/b/315881 [ Sequoia Release x86_64 ] fast/attachment/cocoa/wide-attachment-folder-icon.html [ Pass ImageOnlyFailure ]
diff --git a/Source/ThirdParty/ANGLE/src/tests/gl_tests/OcclusionQueriesTest.cpp b/Source/ThirdParty/ANGLE/src/tests/gl_tests/OcclusionQueriesTest.cpp
index 68952ac962bb..79f3796023f9 100644
--- a/Source/ThirdParty/ANGLE/src/tests/gl_tests/OcclusionQueriesTest.cpp
+++ b/Source/ThirdParty/ANGLE/src/tests/gl_tests/OcclusionQueriesTest.cpp
@@ -1248,6 +1248,140 @@ TEST_P(OcclusionQueriesTestES3, QueryReuseWithMultipleFBOs)
EXPECT_GL_TRUE(result);
}
+// Test that deleting an occlusion query after a failed begin does not cause use-after-free.
+// A prior query primes the pool. The second query triggers pool doubling which under fault
+// injection fails with OOM. Deleting the query and flushing must not crash.
+TEST_P(OcclusionQueriesTestES3, OcclusionQueryDeleteAfterFailedBeginNoUAF)
+{
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::SimpleForPoints(), essl1_shaders::fs::Green());
+ glUseProgram(program);
+ glViewport(0, 0, getWindowWidth(), getWindowHeight());
+
+ // Start render pass and prime the pool with a successful query.
+ glDrawArrays(GL_POINTS, 0, 1);
+ GLQuery q1;
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, q1);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ ASSERT_GL_NO_ERROR();
+
+ // Second query triggers pool doubling. Under fault injection this fails with OOM.
+ GLuint q2;
+ glGenQueries(1, &q2);
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, q2);
+ GLenum err = glGetError();
+ if (err == GL_OUT_OF_MEMORY)
+ {
+ // To get here, a OOM must be injected by hand in visibility buffer doubling.
+ fprintf(stderr, "got expected out of memory\n");
+ }
+ // Delete the query and flush — must not crash.
+ glDeleteQueries(1, &q2);
+ glFinish();
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2 - 1, getWindowHeight() / 2, GLColor::green);
+}
+
+// Test deleting a occlusion query while it is active.
+TEST_P(OcclusionQueriesTestES3, DeleteActiveOcclusionQueryLeavesValidState)
+{
+ GLuint queryId = 0;
+ glGenQueries(1, &queryId);
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, queryId);
+ drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.5f);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ ASSERT_GL_NO_ERROR();
+
+ GLuint queryResult = 0;
+ glGetQueryObjectuiv(queryId, GL_QUERY_RESULT, &queryResult);
+ ASSERT_GL_NO_ERROR();
+ EXPECT_EQ(queryResult, static_cast<GLuint>(GL_TRUE));
+
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, queryId);
+ EXPECT_GL_NO_ERROR();
+
+ drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.5f);
+ ASSERT_GL_NO_ERROR();
+
+ GLint activeQueryId = 0;
+ glGetQueryiv(GL_ANY_SAMPLES_PASSED, GL_CURRENT_QUERY, &activeQueryId);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_EQ(static_cast<GLuint>(activeQueryId), queryId);
+
+ glDeleteQueries(1, &queryId);
+ EXPECT_GL_NO_ERROR();
+
+ EXPECT_GL_FALSE(glIsQuery(queryId));
+
+ // NOTE: This is unspecified and confusing: what should the return value be
+ // if the query has been deleted? If 0, then it's confusing because EndQuery
+ // doesn't fail. If non-zero, how can it return deleted value?
+ // If EndQuery should fail and return should be 0?
+ GLint activeQueryAfterDelete = 0;
+ glGetQueryiv(GL_ANY_SAMPLES_PASSED, GL_CURRENT_QUERY, &activeQueryAfterDelete);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_EQ(static_cast<GLuint>(activeQueryAfterDelete), queryId);
+
+ drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+ EXPECT_GL_NO_ERROR();
+
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ EXPECT_GL_NO_ERROR(); // NOTE: This is unspecified and confusing.
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+ EXPECT_GL_NO_ERROR();
+}
+
+// Test that a query spanning two render passes (broken by glClear) accumulates results correctly.
+// First draw fails depth test (no samples pass), then glClear breaks the render pass, then
+// second draw passes. Query result must be TRUE.
+TEST_P(OcclusionQueriesTestES3, QuerySpansRenderPassBreak)
+{
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::SimpleForPoints(), essl1_shaders::fs::Green());
+ glUseProgram(program);
+ glViewport(0, 0, getWindowWidth(), getWindowHeight());
+
+ glEnable(GL_DEPTH_TEST);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Draw with GL_NEVER does not pass samples.
+ glDepthFunc(GL_NEVER);
+
+ GLQuery query;
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, query);
+ glDrawArrays(GL_POINTS, 0, 1);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ GLuint result = GL_FALSE;
+ glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result);
+ EXPECT_GL_NO_ERROR();
+
+ // Test setup validation: draw with GL_NEVER does not pass samples.
+ EXPECT_GL_FALSE(result);
+ EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2 - 1, getWindowHeight() / 2, GLColor::black);
+
+ // Test the sample accumulation across render passes:
+ // Draw with GL_NEVER -> no samples pass yet.
+ // Break render pass
+ // Draw with GL_ALWAYS -> samples pass.
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, query);
+ glDrawArrays(GL_POINTS, 0, 1);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glDepthFunc(GL_ALWAYS);
+ glDrawArrays(GL_POINTS, 0, 1);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+ EXPECT_GL_NO_ERROR();
+
+ // Query should report TRUE because the second draw passed samples.
+ result = GL_FALSE;
+ glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_GL_TRUE(result);
+
+ EXPECT_PIXEL_COLOR_EQ(getWindowWidth() / 2 - 1, getWindowHeight() / 2, GLColor::green);
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(OcclusionQueriesTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OcclusionQueriesTestES3);