blob: ddba5b333b799e5d537b2b901dd1c7f7f9b640e0 [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());
// http://anglebug.com/500280351
ANGLE_SKIP_TEST_IF(IsIOS());
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
angle::Span<uint8_t> mappedData1;
mtl::BufferSlice slice1;
ASSERT_EQ(bufferPool.allocateAndMap(contextMtl, kLargeSize, &mappedData1, &slice1),
angle::Result::Continue);
EXPECT_EQ(slice1.offset(), 0u);
EXPECT_FALSE(mappedData1.empty());
EXPECT_NE(slice1.buffer(), 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(mappedData1.data(), 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
angle::Span<uint8_t> mappedData2;
mtl::BufferSlice slice2;
ASSERT_EQ(bufferPool.allocateAndMap(contextMtl, kSmallSize, &mappedData2, &slice2),
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.
// The offset should be 0 in the new buffer (not a truncated large value)
EXPECT_EQ(slice2.offset(), 0u);
EXPECT_FALSE(mappedData2.empty());
EXPECT_NE(slice2.buffer(), nullptr);
// Buffers should be different
EXPECT_NE(slice1.buffer().get(), slice2.buffer().get());
// Fill second allocation with a different pattern (0xBB)
memset(mappedData2.data(), 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 = slice1.buffer()->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!";
}
slice1.buffer()->unmap(contextMtl);
bufferPool.destroy(contextMtl);
}
} // namespace
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BufferPoolTest);
ANGLE_INSTANTIATE_TEST(BufferPoolTest, ES2_METAL(), ES3_METAL());