Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create use-after-free detector for Metal textures #7250

Merged
merged 12 commits into from
Oct 20, 2023
2 changes: 1 addition & 1 deletion filament/backend/include/private/backend/DriverAPI.inc
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ DECL_DRIVER_API_R_N(backend::TextureHandle, importTexture,
backend::TextureUsage, usage)

DECL_DRIVER_API_R_N(backend::SamplerGroupHandle, createSamplerGroup,
uint32_t, size)
uint32_t, size, const char*, name)
bejado marked this conversation as resolved.
Show resolved Hide resolved

DECL_DRIVER_API_R_N(backend::RenderPrimitiveHandle, createRenderPrimitive,
backend::VertexBufferHandle, vbh,
Expand Down
14 changes: 14 additions & 0 deletions filament/backend/src/metal/MetalContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#include <Metal/Metal.h>
#include <QuartzCore/QuartzCore.h>

#include <utils/FixedCircularBuffer.h>

#include <array>
#include <atomic>
#include <stack>
Expand Down Expand Up @@ -52,6 +54,11 @@ struct MetalVertexBuffer;

constexpr static uint8_t MAX_SAMPLE_COUNT = 8; // Metal devices support at most 8 MSAA samples

// Keep track of this many most recently freed textures, marking them as "terminated". If the Metal
// backend is asked to bind a texture that has been marked terminated, we throw an Obj-C error,
// which is helpful for debugging use-after-free issues in release builds.
constexpr static size_t kMetalFreedTextureListSize = 64;

struct MetalContext {
MetalDriver* driver;
id<MTLDevice> device = nullptr;
Expand Down Expand Up @@ -111,6 +118,13 @@ struct MetalContext {
tsl::robin_set<MetalSamplerGroup*> samplerGroups;
tsl::robin_set<MetalTexture*> textures;

// This circle buffer implements delayed destruction for Metal texture handles. It keeps a
// handle to the most recent kMetalFreedTextureListSize texture handles. When we're asked to
// destroy a texture handle, we free its texture memory, but keep the MetalTexture object alive,
// marking it as "terminated". If we later are asked to use that texture, we can check its
// terminated status and throw an error instead of crashing.
utils::FixedCircularBuffer<Handle<HwTexture>, kMetalFreedTextureListSize> texturesToDestroy;

MetalBufferPool* bufferPool;

MetalSwapChain* currentDrawSwapChain = nil;
Expand Down
47 changes: 36 additions & 11 deletions filament/backend/src/metal/MetalDriver.mm
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@
target, levels, format, samples, width, height, depth, usage, metalTexture));
}

void MetalDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t size) {
mContext->samplerGroups.insert(construct_handle<MetalSamplerGroup>(sbh, size));
void MetalDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t size, const char* name) {
mContext->samplerGroups.insert(construct_handle<MetalSamplerGroup>(sbh, size, name));
}

void MetalDriver::createRenderPrimitiveR(Handle<HwRenderPrimitive> rph,
Expand Down Expand Up @@ -530,8 +530,19 @@
return;
}

mContext->textures.erase(handle_cast<MetalTexture>(th));
destruct_handle<MetalTexture>(th);
auto* metalTexture = handle_cast<MetalTexture>(th);
mContext->textures.erase(metalTexture);

// Free memory from the texture and mark it as freed.
metalTexture->terminate();

// Delay the destruction of this texture handle.
if (mContext->texturesToDestroy.full()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (mContext->texturesToDestroy.full()) {
if (UTILS_LIKELY(mContext->texturesToDestroy.full())) {

Handle<HwTexture> handleToFree = mContext->texturesToDestroy.pop();
destruct_handle<MetalTexture>(handleToFree);
}
assert_invariant(!mContext->texturesToDestroy.full());
mContext->texturesToDestroy.push(th);
}

void MetalDriver::destroyRenderTarget(Handle<HwRenderTarget> rth) {
Expand All @@ -557,6 +568,12 @@
}

void MetalDriver::terminate() {
// Terminate any oustanding MetalTextures.
while (mContext->texturesToDestroy.size() > 0) {
bejado marked this conversation as resolved.
Show resolved Hide resolved
Handle<HwTexture> toDestroy = mContext->texturesToDestroy.pop();
destruct_handle<MetalTexture>(toDestroy);
}

// finish() will flush the pending command buffer and will ensure all GPU work has finished.
// This must be done before calling bufferPool->reset() to ensure no buffers are in flight.
finish();
Expand Down Expand Up @@ -854,22 +871,26 @@
assert_invariant(sb->size == data.size / sizeof(SamplerDescriptor));
auto const* const samplers = (SamplerDescriptor const*) data.buffer;

#ifndef NDEBUG
// In debug builds, verify that all the textures in the sampler group are still alive.
// Verify that all the textures in the sampler group are still alive.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunate that this whole loop is technically not needed.

// These bugs lead to memory corruption and can be difficult to track down.
for (size_t s = 0; s < data.size / sizeof(SamplerDescriptor); s++) {
if (!samplers[s].t) {
continue;
}
// The difference between this check and the one below is that in release, we do this for
// only a set number of recently freed textures, while the debug check is exhaustive.
auto* metalTexture = handle_cast<MetalTexture>(samplers[s].t);
metalTexture->checkUseAfterFree(sb->debugName.c_str_safe(), s);
#ifndef NDEBUG
auto iter = mContext->textures.find(handle_cast<MetalTexture>(samplers[s].t));
if (iter == mContext->textures.end()) {
utils::slog.e << "updateSamplerGroup: texture #"
<< (int) s << " is dead, texture handle = "
<< samplers[s].t << utils::io::endl;
}
assert_invariant(iter != mContext->textures.end());
}
#endif
}

// Create a MTLArgumentEncoder for these textures.
// Ideally, we would create this encoder at createSamplerGroup time, but we need to know the
Expand Down Expand Up @@ -1357,23 +1378,27 @@

id<MTLCommandBuffer> cmdBuffer = getPendingCommandBuffer(mContext);

#ifndef NDEBUG
// In debug builds, verify that all the textures in the sampler group are still alive.
// Verify that all the textures in the sampler group are still alive.
// These bugs lead to memory corruption and can be difficult to track down.
const auto& handles = samplerGroup->getTextureHandles();
for (size_t s = 0; s < handles.size(); s++) {
if (!handles[s]) {
continue;
}
auto iter = mContext->textures.find(handle_cast<MetalTexture>(handles[s]));
// The difference between this check and the one below is that in release, we do this for
// only a set number of recently freed textures, while the debug check is exhaustive.
auto* metalTexture = handle_cast<MetalTexture>(handles[s]);
metalTexture->checkUseAfterFree(samplerGroup->debugName.c_str_safe(), s);
#ifndef NDEBUG
auto iter = mContext->textures.find(metalTexture);
if (iter == mContext->textures.end()) {
utils::slog.e << "finalizeSamplerGroup: texture #"
<< (int) s << " is dead, texture handle = "
<< handles[s] << utils::io::endl;
}
assert_invariant(iter != mContext->textures.end());
}
#endif
}

utils::FixedCapacityVector<id<MTLTexture>> newTextures(samplerGroup->size, nil);
for (size_t binding = 0; binding < samplerGroup->size; binding++) {
Expand Down
14 changes: 11 additions & 3 deletions filament/backend/src/metal/MetalHandles.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ class MetalTexture : public HwTexture {

MTLPixelFormat devicePixelFormat;

// Frees memory associated with this texture and marks it as "terminated".
// Used to track "use after free" scenario.
void terminate() noexcept;
bool isTerminated() const noexcept { return terminated; }
void checkUseAfterFree(const char* samplerGroupDebugName, size_t textureIndex);

private:
void loadSlice(uint32_t level, MTLRegion region, uint32_t byteOffset, uint32_t slice,
PixelBufferDescriptor const& data) noexcept;
Expand All @@ -256,12 +262,15 @@ class MetalTexture : public HwTexture {

uint32_t minLod = UINT_MAX;
uint32_t maxLod = 0;

bool terminated = false;
bejado marked this conversation as resolved.
Show resolved Hide resolved
};

class MetalSamplerGroup : public HwSamplerGroup {
public:
explicit MetalSamplerGroup(size_t size) noexcept
explicit MetalSamplerGroup(size_t size, const char* name) noexcept
: size(size),
debugName(name),
textureHandles(size, Handle<HwTexture>()),
textures(size, nil),
samplers(size, nil) {}
Expand All @@ -271,12 +280,10 @@ class MetalSamplerGroup : public HwSamplerGroup {
textureHandles[index] = th;
}

#ifndef NDEBUG
// This method is only used for debugging, to ensure all texture handles are alive.
const auto& getTextureHandles() const {
return textureHandles;
}
#endif

// Encode a MTLTexture into this SamplerGroup at the given index.
inline void setFinalizedTexture(size_t index, id<MTLTexture> t) {
Expand Down Expand Up @@ -322,6 +329,7 @@ class MetalSamplerGroup : public HwSamplerGroup {
void useResources(id<MTLRenderCommandEncoder> renderPassEncoder);

size_t size;
utils::CString debugName;
bejado marked this conversation as resolved.
Show resolved Hide resolved

public:

Expand Down
23 changes: 23 additions & 0 deletions filament/backend/src/metal/MetalHandles.mm
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,29 @@ void presentDrawable(bool presentFrame, void* user) {
setLodRange(0, levels - 1);
}

void MetalTexture::terminate() noexcept {
texture = nil;
swizzledTextureView = nil;
lodTextureView = nil;
msaaSidecar = nil;
externalImage.set(nullptr);
terminated = true;
}

void MetalTexture::checkUseAfterFree(const char* samplerGroupDebugName, size_t textureIndex) {
if (UTILS_LIKELY(!isTerminated())) {
return;
}
NSString* reason = [NSString stringWithFormat:
@"Filament Metal texture use after free, sampler group = "
@"%s, texture index = %zu",
samplerGroupDebugName, textureIndex];
NSException* useAfterFreeException = [NSException exceptionWithName:@"MetalTextureUseAfterFree"
reason:reason
userInfo:nil];
[useAfterFreeException raise];
}

bejado marked this conversation as resolved.
Show resolved Hide resolved
MetalTexture::~MetalTexture() {
externalImage.set(nullptr);
}
Expand Down
2 changes: 1 addition & 1 deletion filament/backend/src/opengl/OpenGLDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ void OpenGLDriver::createProgramR(Handle<HwProgram> ph, Program&& program) {
CHECK_GL_ERROR(utils::slog.e)
}

void OpenGLDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t size) {
void OpenGLDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t size, const char* name) {
DEBUG_MARKER()

construct<GLSamplerGroup>(sbh, size);
Expand Down
2 changes: 1 addition & 1 deletion filament/backend/src/vulkan/VulkanDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ void VulkanDriver::finish(int dummy) {
FVK_SYSTRACE_END();
}

void VulkanDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t count) {
void VulkanDriver::createSamplerGroupR(Handle<HwSamplerGroup> sbh, uint32_t count, const char* name) {
auto sg = mResourceAllocator.construct<VulkanSamplerGroup>(sbh, count);
mResourceManager.acquire(sg);
}
Expand Down
2 changes: 1 addition & 1 deletion filament/backend/test/test_FeedbackLoops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ TEST_F(BackendTest, FeedbackLoops) {
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize());
auto sgroup = api.createSamplerGroup(samplers.getSize(), "Test");
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));
auto ubuffer = api.createBufferObject(sizeof(MaterialParams),
BufferObjectBinding::UNIFORM, BufferUsage::STATIC);
Expand Down
8 changes: 4 additions & 4 deletions filament/backend/test/test_LoadImage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ TEST_F(BackendTest, UpdateImage2D) {
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize());
auto sgroup = api.createSamplerGroup(samplers.getSize(), "Test");
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));

api.bindSamplers(0, sgroup);
Expand Down Expand Up @@ -394,7 +394,7 @@ TEST_F(BackendTest, UpdateImageSRGB) {
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize());
auto sgroup = api.createSamplerGroup(samplers.getSize(), "Test");
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));

api.bindSamplers(0, sgroup);
Expand Down Expand Up @@ -469,7 +469,7 @@ TEST_F(BackendTest, UpdateImageMipLevel) {
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize());
auto sgroup = api.createSamplerGroup(samplers.getSize(), "Test");
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));

api.bindSamplers(0, sgroup);
Expand Down Expand Up @@ -556,7 +556,7 @@ TEST_F(BackendTest, UpdateImage3D) {
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, sparams});
auto sgroup = api.createSamplerGroup(samplers.getSize());
auto sgroup = api.createSamplerGroup(samplers.getSize(), "Test");
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));

api.bindSamplers(0, sgroup);
Expand Down
4 changes: 2 additions & 2 deletions filament/backend/test/test_MipLevels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ TEST_F(BackendTest, SetMinMaxLevel) {
samplerParams.filterMag = SamplerMagFilter::NEAREST;
samplerParams.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST;
samplers.setSampler(0, { texture, samplerParams });
backend::Handle<HwSamplerGroup> samplerGroup = api.createSamplerGroup(1);
backend::Handle<HwSamplerGroup> samplerGroup = api.createSamplerGroup(1, "Test");
api.updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(api));
api.bindSamplers(0, samplerGroup);

Expand Down Expand Up @@ -242,4 +242,4 @@ TEST_F(BackendTest, SetMinMaxLevel) {
getDriver().purge();
}

} // namespace test
} // namespace test
4 changes: 2 additions & 2 deletions filament/backend/test/test_RenderExternalImage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ TEST_F(BackendTest, RenderExternalImageWithoutSet) {

SamplerGroup samplers(1);
samplers.setSampler(0, { texture, {} });
backend::Handle<HwSamplerGroup> samplerGroup = getDriverApi().createSamplerGroup(1);
backend::Handle<HwSamplerGroup> samplerGroup = getDriverApi().createSamplerGroup(1, "Test");
getDriverApi().updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(getDriverApi()));
getDriverApi().bindSamplers(0, samplerGroup);

Expand Down Expand Up @@ -234,7 +234,7 @@ TEST_F(BackendTest, RenderExternalImage) {

SamplerGroup samplers(1);
samplers.setSampler(0, { texture, {} });
backend::Handle<HwSamplerGroup> samplerGroup = getDriverApi().createSamplerGroup(1);
backend::Handle<HwSamplerGroup> samplerGroup = getDriverApi().createSamplerGroup(1, "Test");
getDriverApi().updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(getDriverApi()));
getDriverApi().bindSamplers(0, samplerGroup);

Expand Down
2 changes: 1 addition & 1 deletion filament/src/PerViewUniforms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ PerViewUniforms::PerViewUniforms(FEngine& engine) noexcept
: mSamplers(PerViewSib::SAMPLER_COUNT) {
DriverApi& driver = engine.getDriverApi();

mSamplerGroupHandle = driver.createSamplerGroup(mSamplers.getSize());
mSamplerGroupHandle = driver.createSamplerGroup(mSamplers.getSize(), "Per-view samplers");

mUniformBufferHandle = driver.createBufferObject(mUniforms.getSize(),
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC);
Expand Down
5 changes: 3 additions & 2 deletions filament/src/details/MaterialInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,

if (!material->getSamplerInterfaceBlock().isEmpty()) {
mSamplers = other->getSamplerGroup();
mSbHandle = driver.createSamplerGroup(mSamplers.getSize());
mSbHandle = driver.createSamplerGroup(mSamplers.getSize(), mMaterial->getName().c_str());
bejado marked this conversation as resolved.
Show resolved Hide resolved
}

if (material->hasDoubleSidedCapability()) {
Expand Down Expand Up @@ -115,7 +115,8 @@ void FMaterialInstance::initDefaultInstance(FEngine& engine, FMaterial const* ma

if (!material->getSamplerInterfaceBlock().isEmpty()) {
mSamplers = SamplerGroup(material->getSamplerInterfaceBlock().getSize());
mSbHandle = driver.createSamplerGroup(mSamplers.getSize());
mSbHandle = driver.createSamplerGroup(
mSamplers.getSize(), "Material samplers (default material)");
}

const RasterState& rasterState = material->getRasterState();
Expand Down
3 changes: 2 additions & 1 deletion filament/src/details/MorphTargetBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ FMorphTargetBuffer::FMorphTargetBuffer(FEngine& engine, const Builder& builder)
TextureUsage::DEFAULT);

// create and update sampler group
mSbHandle = driver.createSamplerGroup(PerRenderPrimitiveMorphingSib::SAMPLER_COUNT);
mSbHandle = driver.createSamplerGroup(
PerRenderPrimitiveMorphingSib::SAMPLER_COUNT, "Morph target samplers");
SamplerGroup samplerGroup(PerRenderPrimitiveMorphingSib::SAMPLER_COUNT);
samplerGroup.setSampler(PerRenderPrimitiveMorphingSib::POSITIONS, { mPbHandle, {}});
samplerGroup.setSampler(PerRenderPrimitiveMorphingSib::TANGENTS, { mTbHandle, {}});
Expand Down
3 changes: 2 additions & 1 deletion filament/src/details/SkinningBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ FSkinningBuffer::HandleIndicesAndWeights FSkinningBuffer::createIndicesAndWeight
getSkinningBufferWidth(count),
getSkinningBufferHeight(count), 1,
TextureUsage::DEFAULT);
samplerHandle = driver.createSamplerGroup(PerRenderPrimitiveSkinningSib::SAMPLER_COUNT);
samplerHandle = driver.createSamplerGroup(
PerRenderPrimitiveSkinningSib::SAMPLER_COUNT, "Skinning buffer samplers");
SamplerGroup samplerGroup(PerRenderPrimitiveSkinningSib::SAMPLER_COUNT);
samplerGroup.setSampler(PerRenderPrimitiveSkinningSib::BONE_INDICES_AND_WEIGHTS,
{ textureHandle, {}});
Expand Down
1 change: 1 addition & 0 deletions libs/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ set(TEST_SRCS
test/test_CyclicBarrier.cpp
test/test_Entity.cpp
test/test_FixedCapacityVector.cpp
test/test_FixedCircularBuffer.cpp
test/test_Hash.cpp
test/test_JobSystem.cpp
test/test_QuadTreeArray.cpp
Expand Down
Loading
Loading