From 50113d5cc5927fcfc6b779b2e1e8afdd2d7d0952 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Fri, 12 Jun 2026 09:26:08 -0700 Subject: [PATCH] NativeEngine: cache numMips to fix use-after-free in texture upload loop LoadTextureFromImage (and the single-mip-per-face branch of LoadCubeTextureFromImages) drove its mip loop with the condition `mip < image->m_numMips`, re-reading image->m_numMips every iteration. On the last iteration the mip's bgfx::makeRef hands ownership of `image` to bgfx's release queue (releaseFn calls bimg::imageFree(image)). bgfx may run that release on the render thread during bgfx::frame() as soon as the Update2D/UpdateCube command is submitted. The loop then evaluates its exit condition by dereferencing image->m_numMips on memory that has just been freed; if the stale read happens to exceed mip, the loop re-enters imageGetRawData() on the freed container, whose garbage m_format indexes s_imageBlockInfo out of bounds -> intermittent access violation (bimg/image.cpp). This is a use-after-free (CWE-416), reproduced as a rare (~2-4%) crash on texture-loading validation tests. Note: bgfx resource creation/update are themselves mutex-guarded (m_resourceApiLock, also held by bgfx::frame()) and are safe to call from a worker thread; the bug is BabylonNative dereferencing `image` after transferring its ownership to bgfx's async release queue, not a bgfx race. Fix: cache image->m_numMips in the loop initializer (`for (uint8_t mip = 0, numMips = image->m_numMips; mip < numMips; ++mip)`) so the loop bound is not re-read from `image` after the freeing submit. This restores verbatim the change made by commit e8ee5f74 (#1628) that was reverted by #1652. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Plugins/NativeEngine/Source/NativeEngine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 00c8b017e..6f42500f8 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -318,7 +318,7 @@ namespace Babylon texture->Create2D(static_cast(image->m_width), static_cast(image->m_height), (image->m_numMips > 1), 1, Cast(image->m_format), flags); } - for (uint8_t mip = 0; mip < image->m_numMips; ++mip) + for (uint8_t mip = 0, numMips = image->m_numMips; mip < numMips; ++mip) { bimg::ImageMip imageMip{}; if (bimg::imageGetRawData(*image, 0, mip, image->m_data, image->m_size, imageMip)) @@ -387,7 +387,7 @@ namespace Babylon for (uint8_t side = 0; side < 6; ++side) { bimg::ImageContainer* image{images[side]}; - for (uint8_t mip = 0; mip < image->m_numMips; ++mip) + for (uint8_t mip = 0, numMips = image->m_numMips; mip < numMips; ++mip) { bimg::ImageMip imageMip{}; if (bimg::imageGetRawData(*image, 0, mip, image->m_data, image->m_size, imageMip))