Skip to content

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

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]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions