blob: bf26671f935d42c66df055ba389447bfa8b88741 [file]
//
// Copyright 2025 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// BufferPoolTestMetal.mm:
// White box tests for Metal BufferPool allocation logic, specifically for testing
// size_t overflow scenario.
//
#ifdef UNSAFE_BUFFERS_BUILD
# pragma allow_unsafe_buffers
#endif
#include "test_utils/ANGLETest.h"
#include "test_utils/gl_raii.h"
#include "common/apple_platform_utils.h"
#include "common/mathutil.h"
#include "libANGLE/Context.h"
#include "libANGLE/Display.h"
#include "libANGLE/renderer/metal/ContextMtl.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"
#include "libANGLE/renderer/metal/mtl_buffer_pool.h"
using namespace rx;
using namespace angle;
namespace
{
class BufferPoolTest : public ANGLETest<>
{
protected:
BufferPoolTest()
{
setWindowWidth(1);
setWindowHeight(1);
setConfigRedBits(8);
setConfigGreenBits(8);
setConfigBlueBits(8);
setConfigAlphaBits(8);
}
ContextMtl *getContextMtl()
{
// Get the context through the display, similar to D3D11 white box tests
egl::Display *display = static_cast<egl::Display *>(getEGLWindow()->getDisplay());
gl::ContextID contextID = {
static_cast<GLuint>(reinterpret_cast<uintptr_t>(getEGLWindow()->getContext()))};
gl::Context *context = display->getContext(contextID);
return mtl::GetImpl(context);
}
};
// Test that BufferPool correctly handles allocations with size_t offsets > UINT32_MAX
TEST_P(BufferPoolTest, AllocationOffsetNoTruncation)
{
ANGLE_SKIP_TEST_IF(!IsMetalRendererAvailable());
ContextMtl *contextMtl = getContextMtl();
ASSERT_NE(contextMtl, nullptr);
mtl::BufferPool bufferPool;
// Use a large buffer size that will cause offset calculations to exceed UINT32_MAX
// when aligned.
constexpr size_t kLargeSize = 0xFFFFFFFF; // UINT32_MAX + 512 (4GB bytes)
constexpr size_t kAlignment = 256; // Typical Metal buffer alignment
constexpr size_t kSmallSize = 1024; // Second allocation size
// Initialize buffer pool with large initial size
bufferPool.initialize(contextMtl, kLargeSize, kAlignment, 10);
// Perform first allocation
uint8_t *ptr1 = nullptr;
mtl::BufferRef buf1 = nullptr;
size_t offset1 = 0;
bool newBuffer1 = false;
ASSERT_EQ(bufferPool.allocate(contextMtl, kLargeSize, &ptr1, &buf1, &offset1, &newBuffer1),
angle::Result::Continue);
EXPECT_TRUE(newBuffer1);
EXPECT_EQ(offset1, 0u);
EXPECT_NE(ptr1, nullptr);
EXPECT_NE(buf1, nullptr);
// Fill first allocation with a known pattern (0xAA)
// We only fill the first 4KB to avoid spending too much time on this test
constexpr size_t kPatternSize = 4096;
memset(ptr1, 0xAA, kPatternSize);
// Commit the first allocation to ensure it's written to the buffer
ASSERT_EQ(bufferPool.commit(contextMtl), angle::Result::Continue);
// Perform second allocation
uint8_t *ptr2 = nullptr;
mtl::BufferRef buf2 = nullptr;
size_t offset2 = 0;
bool newBuffer2 = false;
ASSERT_EQ(bufferPool.allocate(contextMtl, kSmallSize, &ptr2, &buf2, &offset2, &newBuffer2),
angle::Result::Continue);
// With the fix (size_t), a new buffer should be allocated since the calculated offset
// exceeds the buffer size. Otherwise (no fix), the offset would truncate and
// potentially reuse the same buffer incorrectly, causing memory corruption.
EXPECT_TRUE(newBuffer2);
// The offset should be 0 in the new buffer (not a truncated large value)
EXPECT_EQ(offset2, 0u);
EXPECT_NE(ptr2, nullptr);
EXPECT_NE(buf2, nullptr);
// Buffers should be different
EXPECT_NE(buf1.get(), buf2.get());
// Fill second allocation with a different pattern (0xBB)
memset(ptr2, 0xBB, kSmallSize);
// Commit the second allocation
ASSERT_EQ(bufferPool.commit(contextMtl), angle::Result::Continue);
// Now verify that the first buffer's data wasn't corrupted
// With the uint32_t bug, ptr2 would have overwritten ptr1's data at offset 0
// because the offset wrapped around to 0 or a small value.
// Map the first buffer again to verify its contents
angle::Span<const uint8_t> verifyPtr1 = buf1->mapReadOnly(contextMtl);
ASSERT_FALSE(verifyPtr1.empty());
// Check that the first pattern (0xAA) is still intact
// If the bug exists, this would have been overwritten with 0xBB
for (size_t i = 0; i < kSmallSize && i < kPatternSize; ++i)
{
EXPECT_EQ(verifyPtr1[i], 0xAA)
<< "First buffer corrupted at byte " << i
<< " - uint32_t truncation bug likely caused second allocation to overlap!";
}
buf1->unmap(contextMtl);
bufferPool.destroy(contextMtl);
}
} // namespace
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BufferPoolTest);
ANGLE_INSTANTIATE_TEST(BufferPoolTest, ES2_METAL(), ES3_METAL());