CVE-2026-10233
Description
A global-buffer-overflow in Assimp's Half-Life 1 MDL loader allows local attackers to read out-of-bounds memory via a crafted MDL file.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A global-buffer-overflow in Assimp's Half-Life 1 MDL loader allows local attackers to read out-of-bounds memory via a crafted MDL file.
Vulnerability
The vulnerability resides in the HL1MDLLoader::read_sequence_infos() function within code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp of the Assimp library (versions up to 6.0.4). When parsing a malformed Half-Life 1 MDL file containing an extremely long bone name that completely fills the aiString buffer, an out-of-bounds read of 1023 bytes occurs during the aiString copy operation, accessing memory in the global data section [1].
Exploitation
Exploitation requires local access to the system. The attacker must supply a specially crafted MDL file to the Assimp library. The vulnerability is triggered automatically when the library processes the file, specifically during the read_sequence_infos call. A proof-of-concept (PoC) has been publicly disclosed [1].
Impact
Successful exploitation results in an out-of-bounds memory read, potentially disclosing sensitive information from the global data section. The CVSS v3 score is 3.3 (Low), indicating limited impact. The vulnerability does not enable code execution or privilege escalation [1].
Mitigation
As of the publication date, no official fix has been released. The project has acknowledged the issue as a bug [1]. Users should monitor the Assimp repository for updates [2]. A workaround is to avoid processing untrusted MDL files until a patch is available [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 length validation on bone names read from untrusted MDL files allows a fixed-size aiString buffer to be fully filled, causing an out-of-bounds read during copy."
Attack vector
An attacker provides a specially crafted Half-Life 1 MDL file containing an extremely long bone name that completely fills the fixed-size `aiString::data[1024]` buffer. When Assimp parses this file via the HL1MDLLoader, the copy operation at line 1120 performs a 1023-byte `memcpy` that reads past the end of the buffer into the global data section [CWE-125]. The attack requires local access and low privileges, and no user interaction beyond opening the file [ref_id=1].
Affected code
The vulnerability resides in `HL1MDLLoader::read_sequence_infos()` within `code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp`. The function reads bone names from a malformed Half-Life 1 MDL file without validating their length, then stores them into fixed-size `aiString` buffers. At line 1120, copying this fully-filled `aiString` triggers a 1023-byte out-of-bounds read into adjacent global data [ref_id=1].
What the fix does
No patch has been published by the project; the issue was tagged as a bug but remains unfixed in the repository as of the advisory [ref_id=1]. To remediate the vulnerability, the parser must validate the length of bone names read from the MDL file before copying them into `aiString` buffers, truncating or rejecting names that would overflow the fixed-size data array. Without such a length check, any malformed file can trigger the out-of-bounds read.
Preconditions
- inputThe attacker must supply a malformed Half-Life 1 MDL file with an overly long bone name.
- networkThe attack requires local access to the system running Assimp.
- authThe attacker needs low-privilege local access (CVSS PR:L).
Reproduction
Extract the PoC from the provided zip archive and run: ``` ./assimp_fuzzer ./poc.mdl ``` This triggers a global-buffer-overflow detected by AddressSanitizer at `HL1MDLLoader::read_sequence_infos()` [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