Skip to content

Commit b320866

Browse files
bkaradzic-microsoftbkaradzicCopilot
authored
NativeEngine: fix use-after-free in async texture upload mip loop (#1758)
## Problem `NativeEngine::LoadTextureFromImage` (and the single-mip-per-face branch of `LoadCubeTextureFromImages`) drove the mip-upload loop with `for (uint8_t mip = 0; mip < image->m_numMips; ++mip)`, re-reading `image->m_numMips` for the exit condition on every iteration. On the **last** iteration the mip is submitted to bgfx via `bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)`, where `releaseFn` calls `bimg::imageFree(image)`. bgfx invokes that release once it has consumed the memory — which happens on the render thread inside `bgfx::frame()`, potentially **immediately** after `Update2D`/`UpdateCube` submits the command. The loop then evaluates `mip < image->m_numMips` by dereferencing `image` *after it may already have been freed*. If the stale `m_numMips` read happens to be greater than `mip`, the loop re-enters `bimg::imageGetRawData(*image, ...)` on the freed container; its garbage `m_format` indexes `s_imageBlockInfo` out of bounds → an intermittent access violation. This is a **use-after-free (CWE-416)**, reproduced as a rare (~2–4%) crash on texture-loading validation tests (e.g. an NME particle test that loads a ramp texture while the render loop is active). ### Not a bgfx threading bug bgfx resource creation/update **are** mutex-guarded (`m_resourceApiLock`, which `bgfx::frame()` also holds), so calling them from the decode worker thread is safe and supported. The defect is purely that BabylonNative keeps dereferencing `image` after handing its ownership to bgfx's asynchronous release queue. ## 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 no longer re-read from `image` after the freeing submit. This restores **verbatim** the change made in [`e8ee5f7`](e8ee5f7) (#1628) — see Regression history below. ## Regression history This is a **re-fix of a previously-fixed bug**. The exact same race was fixed in April by commit [`e8ee5f7`](e8ee5f7) ("Fixed race condition when passing data to bgfx. Issue #1398", #1628), which changed both loops to `for (uint8_t mip = 0, numMips = image->m_numMips; ...)`. That fix was inadvertently reverted by commit [`6bb8014`](6bb8014) ("Reworked threading model.", #1652), which restored the original `for (uint8_t mip = 0; mip < image->m_numMips; ++mip)` form in both `LoadTextureFromImage` and `LoadCubeTextureFromImages`, reintroducing the use-after-free. This PR re-applies the original fix unchanged. ## Validation - Stress-tested an affected texture-loading validation test **3000× headless** (30 × 100, 5-way concurrent for extra scheduler pressure): **0 crashes** (baseline crashes ~8% / ~1-in-12). At that baseline rate P(0 in 3000) is effectively zero, so the use-after-free is eliminated. - Particle validation subset: no regressions. Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 53a0432 commit b320866

1 file changed

Lines changed: 2 additions & 2 deletions

File tree

Plugins/NativeEngine/Source/NativeEngine.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ namespace Babylon
323323
texture->Create2D(static_cast<uint16_t>(image->m_width), static_cast<uint16_t>(image->m_height), (image->m_numMips > 1), 1, Cast(image->m_format), flags);
324324
}
325325

326-
for (uint8_t mip = 0; mip < image->m_numMips; ++mip)
326+
for (uint8_t mip = 0, numMips = image->m_numMips; mip < numMips; ++mip)
327327
{
328328
bimg::ImageMip imageMip{};
329329
if (bimg::imageGetRawData(*image, 0, mip, image->m_data, image->m_size, imageMip))
@@ -392,7 +392,7 @@ namespace Babylon
392392
for (uint8_t side = 0; side < 6; ++side)
393393
{
394394
bimg::ImageContainer* image{images[side]};
395-
for (uint8_t mip = 0; mip < image->m_numMips; ++mip)
395+
for (uint8_t mip = 0, numMips = image->m_numMips; mip < numMips; ++mip)
396396
{
397397
bimg::ImageMip imageMip{};
398398
if (bimg::imageGetRawData(*image, 0, mip, image->m_data, image->m_size, imageMip))

0 commit comments

Comments
 (0)