CVE-2026-10229
Description
Heap-buffer-overflow in Assimp's Half-Life 1 MDL loader allows local attackers to crash or potentially execute code via a crafted MDL file.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Heap-buffer-overflow in Assimp's Half-Life 1 MDL loader allows local attackers to crash or potentially execute code via a crafted MDL file.
Vulnerability
A heap-buffer-overflow vulnerability exists in Assimp versions up to 6.0.4 [2] in the HL1MDLLoader::read_meshes() function within HL1MDLLoader.cpp. The flaw occurs when parsing a malformed Half-Life 1 MDL file where the file header declares a number of bones (e.g., 1) but vertex data references a bone index (e.g., 5) that exceeds the allocated temp_bones_ array, leading to an out-of-bounds read [1].
Exploitation
An attacker must provide a specially crafted MDL file to a user or application that uses the Assimp library to load it. No authentication or special privileges are required beyond local file access. The exploit is triggered during the parsing of the MDL file, specifically in the read_meshes() method when it attempts to transform a vertex using a bone matrix from an out-of-bounds index. A proof-of-concept (PoC) has been publicly released [1].
Impact
Successful exploitation results in a heap-buffer-overflow read, which can cause a crash (denial of service) or potentially lead to arbitrary code execution depending on the memory layout and compiler protections. The attack is limited to local execution, meaning the attacker must be able to supply the file to the vulnerable process [1].
Mitigation
As of the publication date (2026-06-01), no official patch has been released. The issue is tracked in the Assimp GitHub repository [1]. Users should monitor for updates and apply a fix when available. Until then, avoid loading untrusted MDL files with Assimp versions 6.0.4 and earlier. The project has acknowledged the bug [1].
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 bounds check on bone index in HL1MDLLoader::read_meshes() allows heap-buffer-overflow when vertex data references a bone index larger than the number of bones declared in the file header."
Attack vector
An attacker provides a crafted Half-Life 1 MDL file where the file header declares a small number of bones (e.g., 1) but the vertex data contains a bone index (e.g., 5) that exceeds that count [ref_id=1]. When Assimp parses this file, `read_meshes()` uses the attacker-controlled bone index to access `temp_bones_` without a bounds check, causing a heap-buffer-overflow [CWE-122]. The attack requires local execution (CVSS:3.1/AV:L) and the attacker must have the ability to supply a malformed MDL file to the Assimp library.
Affected code
The vulnerability resides in `HL1MDLLoader::read_meshes()` within `code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp`. The function uses a bone index read directly from the malformed MDL file to access the `temp_bones_` vector without validating that the index is within bounds. The vector was sized by `read_bones()` based on `header_->numbones`, which in the crashing file is only 1, but the vertex data references bone index 5 [ref_id=1].
What the fix does
The advisory does not include a published patch, but the root cause is clear: `read_meshes()` must validate that each vertex's bone index (`pvertbone[k]`) is strictly less than `temp_bones_.size()` before using it as an array index [ref_id=1]. The fix would add a bounds check before the out-of-bounds access at `HL1MDLLoader.cpp:748`; if the index is out of range, the loader should either skip the vertex or clamp the index. Without such validation, any malformed MDL file with mismatched bone counts triggers the overflow.
Preconditions
- inputAttacker must supply a malformed Half-Life 1 MDL file to the Assimp library
- networkAttack requires local execution (CVSS:3.1/AV:L)
- authAttacker needs local user privileges (CVSS:3.1/PR:L)
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