CNA-BUG-096: VulkanRenderer's constructor leaks the Vulkan handles it created when a later construction step throws
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
VulkanRenderer::VulkanRenderer creates the instance, debug messenger, device, swapchain and other raw handles in sequence with no cleanup path, so a throw from a later step such as PickPhysicalDevice leaves them undestroyed.
- Identifier
CNA-BUG-096- Category
- Bug
- Subsystem
- Graphics & renderers
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- GraphicsDevice construction on VULKAN, including runtime fallback from VULKAN to another renderer
Expected behaviour
A failed renderer construction releases what it created, so that a fallback chain can move to the next renderer without leaking, and so that Vulkan object tracking and LeakSanitizer stay clean. VulkanRenderer::~VulkanRenderer is written for partial states (it destroys only the instance when device_ is null), but a destructor never runs for an object whose constructor threw.
Actual behaviour at TARGET
The constructor body calls CreateInstance, SetupDebugMessenger (validation builds), CreateSurface, PickPhysicalDevice, CreateLogicalDevice, CreateSwapchain and then image views, depth resources, render passes, framebuffers, command pool and buffers, sync objects, sampler, descriptor layout and pool and sprite buffers, with no try/catch and no scope guard. The handles are plain members (VkInstance instance_, VkDebugUtilsMessengerEXT debugMessenger_, VkDevice device_, VkSwapchainKHR swapchain_ and more), so only RAII members such as platformSurface_ are cleaned up while the exception unwinds. PickPhysicalDevice throws "Vulkan: no GPU found" or "Vulkan: no suitable GPU" after the instance and surface exist, so every failed attempt on a host without a usable Vulkan device leaks the instance (and the debug messenger in validation builds); a throw after CreateLogicalDevice also leaks the device and everything created on it.
Source locations
modules/renderers/vulkan/src/VulkanRenderer.cpp— VulkanRenderer::VulkanRenderer (no cleanup path), VulkanRenderer::PickPhysicalDevice and VulkanRenderer::~VulkanRenderermodules/renderers/vulkan/include/CNA/Internal/Renderers/Vulkan/VulkanRenderer.hpp— raw handle members instance_, debugMessenger_, device_, swapchain_modules/renderers/vulkan/examples/vulkan_unavailable_device_skip.cpp— VULKAN-013: the two routine construction failures
Evidence
Checked by reading at 009d40f5; not executed. In C++ only fully constructed members are destroyed when a constructor throws; the class destructor does not run. VULKAN-013's terminate handler exists because PickPhysicalDevice's two refusals are routine on machines without a usable device. SDL_GPU has a comparable guard and test (SdlGpu_ConstructorExceptionSafety); VULKAN has neither.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
vulkan_unavailable_device_skip.cpp turns the two expected failures into a CTest skip; the VULKAN-408 validation-output gate reports leaks only for a device that is destroyed. No test constructs the renderer with a failure injected after CreateInstance.
Regression test
Give the constructor a cleanup path (a function-try-block or a scope guard calling a teardown that tolerates partial state) and add a failure-injection seam after each creation step, asserting under the validation layer's object tracking that no handle survives a failed construction.
Blast radius
VULKAN only, on hosts where construction fails part-way (no suitable GPU, surface or swapchain failure, out of memory). A fallback chain that continues to another renderer keeps the leaked handles for the life of the process, and sanitizer or validation-layer runs on such hosts report them. Successful constructions are unaffected.
Workaround
No workaround is known.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Runtime renderer selection: fallback
- Maintainer workflow
- Debug shutdown and lifetime
- Known issues
- Bug index