Skip to content

Destroying a second Engine crashes (double free of DebugUtils::mSingleton) #10357

Description

@patrikhuber

Describe the bug

In 1.75.1, once two or more filament::Engine instances have been alive at the same time, the
second Engine::destroy() crashes. 1.67.0 is fine. Reproduced on Linux and Windows with a
program that does nothing but create and destroy engines: no meshes, materials, rendering or swap
chains, and destroy order is irrelevant.

Cause: VulkanDriver::terminate() deletes the process-wide static DebugUtils::mSingleton
unconditionally. In 1.67.0 that delete sat inside #if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS), so it
never ran in a release build.

1.67.0:

#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS)
    assert_invariant(DebugUtils::mSingleton);
    delete DebugUtils::mSingleton;
#endif

1.75.1:

    assert_invariant(DebugUtils::mSingleton);
    delete DebugUtils::mSingleton;

delete does not null the pointer and DebugUtils::get() never recreates it, so the second
engine's terminate() deletes it again. ~DebugUtils() then reads mInstance and
mDebugMessenger out of the freed object and calls vkDestroyDebugUtilsMessengerEXT. That gives
either a detected double free or a call through a garbage pointer, which is the two signatures
below. Since the state is global, only the NUMBER of engines matters, not the order.

Introduced by c2341d1 "vk: use debug flag to enable debug utils names (#10220)". That PR moved
debug-utils naming from compile-time guards to a runtime flag and removed
#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS) from every site in the file, including this delete, which
was only safe because it was compiled out of release builds.

To Reproduce

#include <filament/Engine.h>
#include <vector>
int main(int argc, char** argv) {
    const int n = argc > 1 ? atoi(argv[1]) : 1;
    std::vector<filament::Engine*> engines;
    for (int i = 0; i < n; ++i)
        engines.push_back(filament::Engine::create(filament::Engine::Backend::VULKAN));
    for (int i = 0; i < n; ++i)
        filament::Engine::destroy(&engines[i]);   // crashes on i == 1
    return 0;
}

Run with an argument of 2 or more.

Expected behavior

Destroying several engines succeeds, as it does in 1.67.0. Possible fixes: null the pointer after
deleting, reference-count the singleton so only the last terminate() frees it, or gate the
delete on the same runtime flag the rest of the feature now uses.

Screenshots

n/a

Logs

engines alive 1.65.0 Windows 1.75.1 Windows 1.67.0 Linux 1.75.1 Linux
1 ok ok ok ok
2 ok 0xC0000374 ok SIGSEGV
3 ok 0xC0000005 ok SIGSEGV
4 ok 0xC0000374 ok SIGSEGV

0xC0000374 is STATUS_HEAP_CORRUPTION, 0xC0000005 an access violation. Sequential
create-then-destroy, never two alive, is always clean.

double free or corruption (out)
Thread "FEngine::loop" received signal SIGABRT
#6  malloc_printerr (str="double free or corruption (out)")
#9  __GI___libc_free (mem=0x7ffabc057df0)
#10 filament::backend::VulkanDriver::terminate ()
#11 filament::FEngine::loop ()

The other signature is the same frame reached through a null pointer:

Thread "FEngine::loop" received signal SIGSEGV
#0  0x0000000000000000 in ?? ()
#1  filament::backend::VulkanDriver::terminate ()
#2  filament::FEngine::loop ()

Desktop (please complete the following information):

  • OS: Windows 11 (x64) with MSVC 14.51, Ubuntu 24.04 with clang-18
  • GPU: Intel Iris Xe / RTX 3500 Ada / RTX 6000
  • Backend: Vulkan

Additional context

n/a

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions