CVE-2026-10231
Description
A heap-buffer-overflow in Assimp's HL1MDLLoader allows local attackers to crash or potentially execute code via a crafted Half-Life MDL file.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A heap-buffer-overflow in Assimp's HL1MDLLoader allows local attackers to crash or potentially execute code via a crafted Half-Life MDL file.
Vulnerability
A heap-based buffer overflow exists in Assimp up to version 6.0.4 in the HL1MDLLoader::extract_anim_value function of the Half-Life 1 MDL loader. When the num.total field of an animation value is zero, the while loop in the function runs indefinitely, causing an out-of-bounds read past the allocated heap buffer. This can lead to a crash or exploitable memory corruption. The issue was reported as a bug in the project's issue tracker [1].
Exploitation
An attacker must have local access to the system and supply a specially crafted Half-Life MDL file (e.g., through the assimp_fuzzer or any application using the Assimp library). No authentication or special privileges are required beyond the ability to load the file. The PoC provided in the issue triggers the overflow by setting num.total to zero, causing the loop to read beyond buffer bounds.
Impact
Successful exploitation results in a heap-buffer-overflow read, potentially causing a denial of service (crash) or, depending on the memory layout, arbitrary code execution within the context of the process using Assimp. The vulnerability has a CVSS v3 score of 5.3 (Medium).
Mitigation
As of the publication date, no official patch has been released. The issue is tagged as a bug and awaits a fix. Users are advised to avoid opening untrusted .mdl files with Assimp until a patched version is available.
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
23e188e676873HL1MDLLoader: fix segfault in destructors (#6537)
1 file changed · +2 −2
code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp+2 −2 modified@@ -971,7 +971,7 @@ void HL1MDLLoader::read_animations() { pseqdesc = get_buffer_data<SequenceDesc_HL1>(header_->seqindex, header_->numseq); - aiAnimation **scene_animations_ptr = scene_->mAnimations = new aiAnimation *[scene_->mNumAnimations]; + aiAnimation **scene_animations_ptr = scene_->mAnimations = new aiAnimation *[scene_->mNumAnimations](); for (int sequence = 0; sequence < header_->numseq; ++sequence, ++pseqdesc) { pseqgroup = get_buffer_data<SequenceGroup_HL1>(header_->seqgroupindex + pseqdesc->seqgroup * sizeof(SequenceGroup_HL1), 1); @@ -992,7 +992,7 @@ void HL1MDLLoader::read_animations() { scene_animation->mTicksPerSecond = pseqdesc->fps; scene_animation->mDuration = static_cast<double>(pseqdesc->fps) * pseqdesc->numframes; scene_animation->mNumChannels = static_cast<unsigned int>(header_->numbones); - scene_animation->mChannels = new aiNodeAnim *[scene_animation->mNumChannels]; + scene_animation->mChannels = new aiNodeAnim *[scene_animation->mNumChannels](); for (int bone = 0; bone < header_->numbones; bone++, ++pbone, ++panim) { aiNodeAnim *node_anim = scene_animation->mChannels[bone] = new aiNodeAnim();
392a658f9c27Bugfix/sparky kitty studios (#6623)
5 files changed · +68 −60
code/AssetLib/FBX/FBXDeformer.cpp+10 −4 modified@@ -45,6 +45,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef ASSIMP_BUILD_NO_FBX_IMPORTER +#include <algorithm> + #include "FBXParser.h" #include "FBXDocument.h" #include "FBXMeshGeometry.h" @@ -144,8 +146,10 @@ BlendShape::BlendShape(uint64_t id, const Element& element, const Document& doc, for (const Connection* con : conns) { const BlendShapeChannel* const bspc = ProcessSimpleConnection<BlendShapeChannel>(*con, false, "BlendShapeChannel -> BlendShape", element); if (bspc) { - auto pr = blendShapeChannels.insert(bspc); - if (!pr.second) { + // Only add a channel if it doesn't exist already + if (std::find(blendShapeChannels.begin(), blendShapeChannels.end(), bspc) == blendShapeChannels.end()) { + blendShapeChannels.push_back(bspc); + } else { FBXImporter::LogWarn("there is the same blendShapeChannel id ", bspc->ID()); } } @@ -170,8 +174,10 @@ BlendShapeChannel::BlendShapeChannel(uint64_t id, const Element& element, const for (const Connection* con : conns) { const ShapeGeometry* const sg = ProcessSimpleConnection<ShapeGeometry>(*con, false, "Shape -> BlendShapeChannel", element); if (sg) { - auto pr = shapeGeometries.insert(sg); - if (!pr.second) { + // Only add a geometry if it doesn't exist already + if (std::find(shapeGeometries.begin(), shapeGeometries.end(), sg) == shapeGeometries.end()) { + shapeGeometries.push_back(sg); + } else { FBXImporter::LogWarn("there is the same shapeGeometrie id ", sg->ID()); } }
code/AssetLib/FBX/FBXDocument.h+4 −4 modified@@ -865,14 +865,14 @@ class BlendShapeChannel final : public Deformer { return fullWeights; } - const std::unordered_set<const ShapeGeometry*>& GetShapeGeometries() const { + const std::vector<const ShapeGeometry*>& GetShapeGeometries() const { return shapeGeometries; } private: float percent; WeightArray fullWeights; - std::unordered_set<const ShapeGeometry*> shapeGeometries; + std::vector<const ShapeGeometry*> shapeGeometries; }; /** DOM class for BlendShape deformers */ @@ -882,12 +882,12 @@ class BlendShape final : public Deformer { virtual ~BlendShape() = default; - const std::unordered_set<const BlendShapeChannel*>& BlendShapeChannels() const { + const std::vector<const BlendShapeChannel*>& BlendShapeChannels() const { return blendShapeChannels; } private: - std::unordered_set<const BlendShapeChannel*> blendShapeChannels; + std::vector<const BlendShapeChannel*> blendShapeChannels; }; /** DOM class for skin deformer clusters (aka sub-deformers) */
code/AssetLib/FBX/FBXMeshGeometry.cpp+6 −3 modified@@ -45,6 +45,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef ASSIMP_BUILD_NO_FBX_IMPORTER +#include <algorithm> #include <functional> #include "FBXMeshGeometry.h" @@ -69,16 +70,18 @@ Geometry::Geometry(uint64_t id, const Element& element, const std::string& name, } const BlendShape* const bsp = ProcessSimpleConnection<BlendShape>(*con, false, "BlendShape -> Geometry", element); if (bsp) { - auto pr = blendShapes.insert(bsp); - if (!pr.second) { + // Only add a blendshape if it doesn't exist already + if (std::find(blendShapes.begin(), blendShapes.end(), bsp) == blendShapes.end()) { + blendShapes.push_back(bsp); + } else { FBXImporter::LogWarn("there is the same blendShape id ", bsp->ID()); } } } } // ------------------------------------------------------------------------------------------------ -const std::unordered_set<const BlendShape*>& Geometry::GetBlendShapes() const { +const std::vector<const BlendShape*>& Geometry::GetBlendShapes() const { return blendShapes; }
code/AssetLib/FBX/FBXMeshGeometry.h+2 −2 modified@@ -72,11 +72,11 @@ class Geometry : public Object { /// @brief Get the BlendShape attached to this geometry or nullptr /// @return The blendshape arrays. - const std::unordered_set<const BlendShape*>& GetBlendShapes() const; + const std::vector<const BlendShape*>& GetBlendShapes() const; private: const Skin* skin; - std::unordered_set<const BlendShape*> blendShapes; + std::vector<const BlendShape*> blendShapes; };
code/PostProcessing/ImproveCacheLocality.cpp+46 −47 modified@@ -58,11 +58,55 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <stack> namespace Assimp { +namespace { + ai_real calculateInputACMR(aiMesh *pMesh, const aiFace *const pcEnd, + unsigned int configCacheDepth, unsigned int meshNum) { + ai_real fACMR = 0.0f; + unsigned int *piFIFOStack = new unsigned int[configCacheDepth]; + memset(piFIFOStack, 0xff, configCacheDepth * sizeof(unsigned int)); + unsigned int *piCur = piFIFOStack; + const unsigned int *const piCurEnd = piFIFOStack + configCacheDepth; + + // count the number of cache misses + unsigned int iCacheMisses = 0; + for (const aiFace *pcFace = pMesh->mFaces; pcFace != pcEnd; ++pcFace) { + for (unsigned int qq = 0; qq < 3; ++qq) { + bool bInCache = false; + for (unsigned int *pp = piFIFOStack; pp < piCurEnd; ++pp) { + if (*pp == pcFace->mIndices[qq]) { + // the vertex is in cache + bInCache = true; + break; + } + } + if (!bInCache) { + ++iCacheMisses; + if (piCurEnd == piCur) { + piCur = piFIFOStack; + } + *piCur++ = pcFace->mIndices[qq]; + } + } + } + delete[] piFIFOStack; + fACMR = (ai_real)iCacheMisses / pMesh->mNumFaces; + if (3.0 == fACMR) { + char szBuff[128]; // should be sufficiently large in every case + + // the JoinIdenticalVertices process has not been executed on this + // mesh, otherwise this value would normally be at least minimally + // smaller than 3.0 ... + ai_snprintf(szBuff, 128, "Mesh %u: Not suitable for vcache optimization", meshNum); + ASSIMP_LOG_WARN(szBuff); + return static_cast<ai_real>(0.f); + } + return fACMR; + } +} // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -ImproveCacheLocalityProcess::ImproveCacheLocalityProcess() : - mConfigCacheDepth(PP_ICL_PTCACHE_SIZE) { +ImproveCacheLocalityProcess::ImproveCacheLocalityProcess() : mConfigCacheDepth(PP_ICL_PTCACHE_SIZE) { // empty } @@ -107,51 +151,6 @@ void ImproveCacheLocalityProcess::Execute(aiScene *pScene) { } } -// ------------------------------------------------------------------------------------------------ -static ai_real calculateInputACMR(aiMesh *pMesh, const aiFace *const pcEnd, - unsigned int configCacheDepth, unsigned int meshNum) { - ai_real fACMR = 0.0f; - unsigned int *piFIFOStack = new unsigned int[configCacheDepth]; - memset(piFIFOStack, 0xff, configCacheDepth * sizeof(unsigned int)); - unsigned int *piCur = piFIFOStack; - const unsigned int *const piCurEnd = piFIFOStack + configCacheDepth; - - // count the number of cache misses - unsigned int iCacheMisses = 0; - for (const aiFace *pcFace = pMesh->mFaces; pcFace != pcEnd; ++pcFace) { - for (unsigned int qq = 0; qq < 3; ++qq) { - bool bInCache = false; - for (unsigned int *pp = piFIFOStack; pp < piCurEnd; ++pp) { - if (*pp == pcFace->mIndices[qq]) { - // the vertex is in cache - bInCache = true; - break; - } - } - if (!bInCache) { - ++iCacheMisses; - if (piCurEnd == piCur) { - piCur = piFIFOStack; - } - *piCur++ = pcFace->mIndices[qq]; - } - } - } - delete[] piFIFOStack; - fACMR = (ai_real)iCacheMisses / pMesh->mNumFaces; - if (3.0 == fACMR) { - char szBuff[128]; // should be sufficiently large in every case - - // the JoinIdenticalVertices process has not been executed on this - // mesh, otherwise this value would normally be at least minimally - // smaller than 3.0 ... - ai_snprintf(szBuff, 128, "Mesh %u: Not suitable for vcache optimization", meshNum); - ASSIMP_LOG_WARN(szBuff); - return static_cast<ai_real>(0.f); - } - return fACMR; -} - // ------------------------------------------------------------------------------------------------ // Improves the cache coherency of a specific mesh ai_real ImproveCacheLocalityProcess::ProcessMesh(aiMesh *pMesh, unsigned int meshNum) {
Vulnerability mechanics
Root cause
"Missing validation of the `num.total` field in `HL1MDLLoader::extract_anim_value` allows a zero value to cause an infinite loop and heap out-of-bounds read."
Attack vector
An attacker provides a crafted Half-Life 1 MDL file where the `num.total` field in an animation value structure is set to zero [ref_id=1]. When `HL1MDLLoader::extract_anim_value` processes this file, the zero value causes a `while` loop to run infinitely, repeatedly advancing a pointer beyond the allocated heap buffer [ref_id=1]. This results in a heap-based buffer over-read that can crash the application or leak memory [CWE-122][CWE-119]. The attack requires local access to load the malicious file [ref_id=1].
Affected code
The vulnerability resides in `HL1MDLLoader::extract_anim_value` within `code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp` at line 1332 [ref_id=1]. The function is called from `read_animations()` during Half-Life 1 MDL file loading [ref_id=1].
What the fix does
The patch does not appear in the provided bundle; however, the advisory [ref_id=1] identifies the root cause as a missing sanity check on `num.total`. A proper fix would validate that `num.total` is non-zero before entering the loop, or add a bounds check on the advancing pointer to prevent it from exceeding the allocated heap region. Without such validation, the loop runs infinitely when `num.total` is zero, causing the out-of-bounds read.
Preconditions
- inputAttacker must supply a crafted Half-Life 1 MDL file with num.total set to zero
- configThe file must be loaded by Assimp's HL1MDLLoader (e.g., via assimp_fuzzer or any application using Assimp)
- networkAttack requires local access to the system to load the malicious file
Reproduction
1. Clone the Assimp repository and build with AddressSanitizer enabled (e.g., `-fsanitize=address -O0 -g`). 2. Obtain the PoC file from the zip archive referenced in [ref_id=1]. 3. Run `./assimp_fuzzer ./poc.mdl`. 4. Observe the ASAN report confirming a heap-buffer-overflow read at `HL1MDLLoader.cpp:1332` [ref_id=1].
Generated on Jun 1, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
1- Assimp: Ten Memory-Safety CVEs Disclosed Across Half-Life MDL, glTF, and FBX ParsersVypr Intelligence · Jun 1, 2026