Skip to content

Use after free in vulkan backend (docking, dynamic rendering, multi-viewport) #9390

@vikhik

Description

@vikhik

Version/Branch of Dear ImGui:

Branch: docking, commit: 417f5ed

Back-ends:

imgui_impl_vulkan.cpp + imgui_impl_sdl3.cpp

Compiler, OS:

Windows 10, zig cc (v0.16.0)

Full config/build information:

Utilising imgui+docking via https://github.com/tiawl/cimgui.zig.git#68c0ed133ed317061baad3a3ae56cd2940c61a62 (a thin zig-build system wrapper for imgui, with c-bindings generated using dear_bindings).

SDL3 + Vulkan

The working demo is tiny, git access can be provided if desired (private gitlab repo).

Details:

I believe there is a use-after-free of &wd->SurfaceFormat.format inside imgui_impl_vulkan.cpp with dynamic rendering on the docking branch.

Disclaimer: I did use Claude to help dig into the issue, but the report is 100% artisanal.

I have a 100% crash repro with a basic hello world setup with SDL3 + Vulkan, using dynamic rendering, when dragging a panel into its own window, returning it and docking it inside the main one, and then moving it into its own window again.

Further below there is a code example below recreates this inside the imgui SDL3 + Vulkan example (not a crash, but via logging).

My zig callstack is:

thread 7188 panic: load of value 2863311530, which is not valid for type 'const VkFormat'
D:\dev\playphyszig\zig-pkg\cimgui_zig-1.0.0-2XubksPPvwDUY1zt1EGL16m1Zr-a53ZGn13PsqjChWhk\dcimgui\docking\backends\imgui_impl_vulkan.cpp:1525: 0x7ff6654aa519 in ImGui_ImplVulkanH_SelectSurfaceFormat (cimgui.lib)
                if (avail_format[avail_i].format == request_formats[request_i] && avail_format[avail_i].colorSpace == request_color_space)

D:\dev\playphyszig\zig-pkg\cimgui_zig-1.0.0-2XubksPPvwDUY1zt1EGL16m1Zr-a53ZGn13PsqjChWhk\dcimgui\docking\backends\imgui_impl_vulkan.cpp:2015: 0x7ff6654b2620 in ImGui_ImplVulkan_CreateWindow (cimgui.lib)
    wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(v->PhysicalDevice, wd->Surface, requestSurfaceImageFormats.Data, requestSurfaceImageFormats.Size, requestSurfaceColorSpace);

D:\dev\playphyszig\zig-pkg\cimgui_zig-1.0.0-2XubksPPvwDUY1zt1EGL16m1Zr-a53ZGn13PsqjChWhk\dcimgui\docking\imgui.cpp:17429: 0x7ff665173c27 in ImGui::UpdatePlatformWindows (cimgui.lib)
                g.PlatformIO.Renderer_CreateWindow(viewport);

D:\dev\playphyszig\zig-pkg\cimgui_zig-1.0.0-2XubksPPvwDUY1zt1EGL16m1Zr-a53ZGn13PsqjChWhk\dcimgui\docking\dcimgui.cpp:2806: 0x7ff664cd583e in ImGui_UpdatePlatformWindows (cimgui.lib)
    ::ImGui::UpdatePlatformWindows();

D:\dev\playphyszig\src\engine\imgui\root.zig:146:34: 0x7ff664bdd17b in updatePlatformWindows (playphys_zcu.obj)
    c.ImGui_UpdatePlatformWindows();

The value is "0xAAAAAAAA" - either uninitialised or freed memory by my zig allocator.

Editing the block ~line 2032 from:

#ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING
        if (wd->UseDynamicRendering)
        {
            pipeline_info->PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
            pipeline_info->PipelineRenderingCreateInfo.colorAttachmentCount = 1;
            pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats = &wd->SurfaceFormat.format;
        }
        else
        {
            pipeline_info->RenderPass = wd->RenderPass;
        }
#endif
        bd->PipelineForViewports = ImGui_ImplVulkan_CreatePipeline(v->Device, v->Allocator, VK_NULL_HANDLE, &v->PipelineInfoForViewports);
    }

to

#ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING
        if (wd->UseDynamicRendering)
        {
            pipeline_info->PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
            pipeline_info->PipelineRenderingCreateInfo.colorAttachmentCount = 1;
            pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats = &wd->SurfaceFormat.format;
            bd->PipelineForViewports = ImGui_ImplVulkan_CreatePipeline(v->Device, v->Allocator, VK_NULL_HANDLE, &v->PipelineInfoForViewports);
            pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats = nullptr;
            pipeline_info->PipelineRenderingCreateInfo.colorAttachmentCount = 0;
        }
        else
        {
            pipeline_info->RenderPass = wd->RenderPass;
            bd->PipelineForViewports = ImGui_ImplVulkan_CreatePipeline(v->Device, v->Allocator, VK_NULL_HANDLE, &v->PipelineInfoForViewports);
        }
#else
        bd->PipelineForViewports = ImGui_ImplVulkan_CreatePipeline(v->Device, v->Allocator, VK_NULL_HANDLE, &v->PipelineInfoForViewports);
#endif
    }

Resolves the crash. I am not certain that this is the correct fix - again, I'm not a rendering engineer - but it does resolve my crash.

My goal for this issue report: confirm if what I'm seeing is correct, report it so you guys know about it, and get it fixed properly with time. I have a local workaround so I'm okay for now.

Minimal, Complete and Verifiable Example code:

First patch is to add dynamic rendering to the SDL3+Vulkan example.
Note: I am not a rendering engineer and this was LLM assisted.

From 7ba9481a3704d10a8c4fdb6e16cb456887aa0fa7 Mon Sep 17 00:00:00 2001
From: Vikram Saran <contact@vikram.codes>
Date: Mon, 4 May 2026 11:46:17 +0200
Subject: [PATCH] Enable dynamic rendering in the SDL3+Vulkan Example

---
 examples/example_sdl3_vulkan/main.cpp | 81 ++++++++++++++++++++++++---
 1 file changed, 72 insertions(+), 9 deletions(-)

diff --git a/examples/example_sdl3_vulkan/main.cpp b/examples/example_sdl3_vulkan/main.cpp
index e961ca1c0..0935971db 100644
--- a/examples/example_sdl3_vulkan/main.cpp
+++ b/examples/example_sdl3_vulkan/main.cpp
@@ -51,6 +51,7 @@ static VkDescriptorPool         g_DescriptorPool = VK_NULL_HANDLE;
 static ImGui_ImplVulkanH_Window g_MainWindowData;
 static uint32_t                 g_MinImageCount = 2;
 static bool                     g_SwapChainRebuild = false;
+static ImVector<VkSemaphore>    g_AcquireSemaphores;
 
 static void check_vk_result(VkResult err)
 {
@@ -152,6 +153,13 @@ static void SetupVulkan(ImVector<const char*> instance_extensions)
     {
         ImVector<const char*> device_extensions;
         device_extensions.push_back("VK_KHR_swapchain");
+#ifdef VK_KHR_dynamic_rendering
+        device_extensions.push_back("VK_KHR_dynamic_rendering");
+        device_extensions.push_back("VK_KHR_depth_stencil_resolve");
+        device_extensions.push_back("VK_KHR_create_renderpass2");
+        device_extensions.push_back("VK_KHR_multiview");
+        device_extensions.push_back("VK_KHR_maintenance2");
+#endif
 
         // Enumerate physical device extension
         uint32_t properties_count;
@@ -170,10 +178,14 @@ static void SetupVulkan(ImVector<const char*> instance_extensions)
         queue_info[0].queueFamilyIndex = g_QueueFamily;
         queue_info[0].queueCount = 1;
         queue_info[0].pQueuePriorities = queue_priority;
+        VkPhysicalDeviceDynamicRenderingFeatures dynamic_rendering_features = {};
+        dynamic_rendering_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES;
+        dynamic_rendering_features.dynamicRendering = VK_TRUE;
         VkDeviceCreateInfo create_info = {};
         create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
         create_info.queueCreateInfoCount = sizeof(queue_info) / sizeof(queue_info[0]);
         create_info.pQueueCreateInfos = queue_info;
+        create_info.pNext = &dynamic_rendering_features;
         create_info.enabledExtensionCount = (uint32_t)device_extensions.Size;
         create_info.ppEnabledExtensionNames = device_extensions.Data;
         err = vkCreateDevice(g_PhysicalDevice, &create_info, g_Allocator, &g_Device);
@@ -237,7 +249,10 @@ static void SetupVulkanWindow(ImGui_ImplVulkanH_Window* wd, VkSurfaceKHR surface
 
 static void CleanupVulkan()
 {
+    vkDeviceWaitIdle(g_Device);
     vkDestroyDescriptorPool(g_Device, g_DescriptorPool, g_Allocator);
+    for (int i = 0; i < g_AcquireSemaphores.Size; i++)
+        vkDestroySemaphore(g_Device, g_AcquireSemaphores[i], g_Allocator);
 
 #ifdef APP_USE_VULKAN_DEBUG_REPORT
     // Remove the debug report callback
@@ -257,9 +272,11 @@ static void CleanupVulkanWindow(ImGui_ImplVulkanH_Window* wd)
 
 static void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* draw_data)
 {
-    VkSemaphore image_acquired_semaphore  = wd->FrameSemaphores[wd->SemaphoreIndex].ImageAcquiredSemaphore;
-    VkSemaphore render_complete_semaphore = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore;
-    VkResult err = vkAcquireNextImageKHR(g_Device, wd->Swapchain, UINT64_MAX, image_acquired_semaphore, VK_NULL_HANDLE, &wd->FrameIndex);
+    VkResult err;
+
+    VkSemaphore acquire_semaphore = g_AcquireSemaphores[wd->SemaphoreIndex];
+    uint32_t image_index = 0;
+    err = vkAcquireNextImageKHR(g_Device, wd->Swapchain, UINT64_MAX, acquire_semaphore, VK_NULL_HANDLE, &image_index);
     if (err == VK_ERROR_OUT_OF_DATE_KHR || err == VK_SUBOPTIMAL_KHR)
         g_SwapChainRebuild = true;
     if (err == VK_ERROR_OUT_OF_DATE_KHR)
@@ -267,6 +284,9 @@ static void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* draw_data)
     if (err != VK_SUBOPTIMAL_KHR)
         check_vk_result(err);
 
+    wd->FrameIndex = image_index;
+    VkSemaphore render_complete_semaphore = wd->FrameSemaphores[wd->FrameIndex].RenderCompleteSemaphore;
+
     ImGui_ImplVulkanH_Frame* fd = &wd->Frames[wd->FrameIndex];
     {
         err = vkWaitForFences(g_Device, 1, &fd->Fence, VK_TRUE, UINT64_MAX);    // wait indefinitely instead of periodically checking
@@ -284,6 +304,27 @@ static void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* draw_data)
         err = vkBeginCommandBuffer(fd->CommandBuffer, &info);
         check_vk_result(err);
     }
+
+    if (wd->UseDynamicRendering)
+    {
+        static PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR = (PFN_vkCmdBeginRenderingKHR)vkGetDeviceProcAddr(g_Device, "vkCmdBeginRenderingKHR");
+        VkRenderingAttachmentInfoKHR color_attachment = {};
+        color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
+        color_attachment.imageView = fd->BackbufferView;
+        color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+        color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
+        color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+        color_attachment.clearValue = wd->ClearValue;
+        VkRenderingInfoKHR rendering_info = {};
+        rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR;
+        rendering_info.renderArea.extent.width = wd->Width;
+        rendering_info.renderArea.extent.height = wd->Height;
+        rendering_info.layerCount = 1;
+        rendering_info.colorAttachmentCount = 1;
+        rendering_info.pColorAttachments = &color_attachment;
+        vkCmdBeginRenderingKHR(fd->CommandBuffer, &rendering_info);
+    }
+    else
     {
         VkRenderPassBeginInfo info = {};
         info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
@@ -300,13 +341,19 @@ static void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* draw_data)
     ImGui_ImplVulkan_RenderDrawData(draw_data, fd->CommandBuffer);
 
     // Submit command buffer
-    vkCmdEndRenderPass(fd->CommandBuffer);
+    if (wd->UseDynamicRendering)
+    {
+        static PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR = (PFN_vkCmdEndRenderingKHR)vkGetDeviceProcAddr(g_Device, "vkCmdEndRenderingKHR");
+        vkCmdEndRenderingKHR(fd->CommandBuffer);
+    }
+    else
+        vkCmdEndRenderPass(fd->CommandBuffer);
     {
         VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
         VkSubmitInfo info = {};
         info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
         info.waitSemaphoreCount = 1;
-        info.pWaitSemaphores = &image_acquired_semaphore;
+        info.pWaitSemaphores = &acquire_semaphore;
         info.pWaitDstStageMask = &wait_stage;
         info.commandBufferCount = 1;
         info.pCommandBuffers = &fd->CommandBuffer;
@@ -324,7 +371,7 @@ static void FramePresent(ImGui_ImplVulkanH_Window* wd)
 {
     if (g_SwapChainRebuild)
         return;
-    VkSemaphore render_complete_semaphore = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore;
+    VkSemaphore render_complete_semaphore = wd->FrameSemaphores[wd->FrameIndex].RenderCompleteSemaphore;
     VkPresentInfoKHR info = {};
     info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
     info.waitSemaphoreCount = 1;
@@ -339,7 +386,7 @@ static void FramePresent(ImGui_ImplVulkanH_Window* wd)
         return;
     if (err != VK_SUBOPTIMAL_KHR)
         check_vk_result(err);
-    wd->SemaphoreIndex = (wd->SemaphoreIndex + 1) % wd->SemaphoreCount; // Now we can use the next set of semaphores
+    wd->SemaphoreIndex = (wd->SemaphoreIndex + 1) % wd->SemaphoreCount;
 }
 
 // Main code
@@ -386,6 +433,16 @@ int main(int, char**)
     SDL_GetWindowSize(window, &w, &h);
     ImGui_ImplVulkanH_Window* wd = &g_MainWindowData;
     SetupVulkanWindow(wd, surface, w, h);
+    {
+        g_AcquireSemaphores.resize(wd->SemaphoreCount);
+        for (uint32_t i = 0; i < wd->SemaphoreCount; i++)
+        {
+            VkSemaphoreCreateInfo info = {};
+            info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+            err = vkCreateSemaphore(g_Device, &info, g_Allocator, &g_AcquireSemaphores[i]);
+            check_vk_result(err);
+        }
+    }
     SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
     SDL_ShowWindow(window);
 
@@ -421,7 +478,7 @@ int main(int, char**)
     // Setup Platform/Renderer backends
     ImGui_ImplSDL3_InitForVulkan(window);
     ImGui_ImplVulkan_InitInfo init_info = {};
-    //init_info.ApiVersion = VK_API_VERSION_1_3;              // Pass in your value of VkApplicationInfo::apiVersion, otherwise will default to header version.
+    init_info.ApiVersion = VK_API_VERSION_1_4;              // Pass in your value of VkApplicationInfo::apiVersion, otherwise will default to header version.
     init_info.Instance = g_Instance;
     init_info.PhysicalDevice = g_PhysicalDevice;
     init_info.Device = g_Device;
@@ -432,11 +489,15 @@ int main(int, char**)
     init_info.MinImageCount = g_MinImageCount;
     init_info.ImageCount = wd->ImageCount;
     init_info.Allocator = g_Allocator;
-    init_info.PipelineInfoMain.RenderPass = wd->RenderPass;
+    init_info.UseDynamicRendering = true;
     init_info.PipelineInfoMain.Subpass = 0;
     init_info.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
+    init_info.PipelineInfoMain.PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
+    init_info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
+    init_info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &wd->SurfaceFormat.format;
     init_info.CheckVkResultFn = check_vk_result;
     ImGui_ImplVulkan_Init(&init_info);
+    wd->UseDynamicRendering = true;
 
     // Load Fonts
     // - If fonts are not explicitly loaded, Dear ImGui will select an embedded font: either AddFontDefaultVector() or AddFontDefaultBitmap().
@@ -503,6 +564,8 @@ int main(int, char**)
         ImGui_ImplVulkan_NewFrame();
         ImGui_ImplSDL3_NewFrame();
         ImGui::NewFrame();
+        ImGui::DockSpaceOverViewport(
+            0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode, nullptr);
 
         // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
         if (show_demo_window)
-- 
2.50.1.windows.1

Second patch is to demonstrate the issue:

From ff4d8d33cd027271c2e2475d850f952f93ffd42c Mon Sep 17 00:00:00 2001
From: Vikram Saran <contact@vikram.codes>
Date: Mon, 4 May 2026 12:21:27 +0200
Subject: [PATCH] Add debug printing for window and pipeline creation

---
 backends/imgui_impl_vulkan.cpp | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/backends/imgui_impl_vulkan.cpp b/backends/imgui_impl_vulkan.cpp
index 8df12926f..c28825129 100644
--- a/backends/imgui_impl_vulkan.cpp
+++ b/backends/imgui_impl_vulkan.cpp
@@ -2088,8 +2088,13 @@ static void ImGui_ImplVulkan_CreateWindow(ImGuiViewport* viewport)
     ImGui_ImplVulkan_PipelineInfo* pipeline_info = &v->PipelineInfoForViewports;
     ImVector<VkFormat> requestSurfaceImageFormats;
 #ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING
+    fprintf(stderr, "[DEBUG] CreateWindow read: pColorAttachmentFormats=%p\n", (void*)pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats);
     for (uint32_t n = 0; n < pipeline_info->PipelineRenderingCreateInfo.colorAttachmentCount; n++)
-        requestSurfaceImageFormats.push_back(pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats[n]);
+    {
+        VkFormat fmt = pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats[n];
+        fprintf(stderr, "[DEBUG] CreateWindow read: format[%d]=%d (0x%x)\n", n, (int)fmt, (int)fmt);
+        requestSurfaceImageFormats.push_back(fmt);
+    }
 #endif
     const VkFormat defaultFormats[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM };
     for (VkFormat format : defaultFormats)
@@ -2111,6 +2116,7 @@ static void ImGui_ImplVulkan_CreateWindow(ImGuiViewport* viewport)
     vd->WindowOwned = true;
 
     // Create pipeline (shared by all secondary viewports)
+    fprintf(stderr, "[DEBUG] CreateWindow: PipelineForViewports=%p\n", (void*)bd->PipelineForViewports);
     if (bd->PipelineForViewports == VK_NULL_HANDLE)
     {
 #ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING
@@ -2118,6 +2124,7 @@ static void ImGui_ImplVulkan_CreateWindow(ImGuiViewport* viewport)
         {
             pipeline_info->PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
             pipeline_info->PipelineRenderingCreateInfo.colorAttachmentCount = 1;
+            fprintf(stderr, "[DEBUG] CreateWindow set: pColorAttachmentFormats=%p (wd->SurfaceFormat.format)\n", (void*)&wd->SurfaceFormat.format);
             pipeline_info->PipelineRenderingCreateInfo.pColorAttachmentFormats = &wd->SurfaceFormat.format;
         }
         else
-- 
2.50.1.windows.1

With the above, the first window creation gives:

[DEBUG] CreateWindow read: pColorAttachmentFormats=0000000000000000
[DEBUG] CreateWindow: PipelineForViewports=0000000000000000
[DEBUG] CreateWindow set: pColorAttachmentFormats=00000191CF9C6A70 (wd->SurfaceFormat.format)

And the second gives

[DEBUG] CreateWindow read: pColorAttachmentFormats=00000191CF9C6A70
[DEBUG] CreateWindow read: format[0]=-572662307 (0xdddddddd)
[DEBUG] CreateWindow: PipelineForViewports=00004A000000004A

I believe this shows a use after free for format[0]

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions