From c39d8c15dbbe03174af61d8eedbbf90120f4eb9f Mon Sep 17 00:00:00 2001
From: Alb3e3 <74142887+Alb3e3@users.noreply.github.com>
Date: Sun, 28 Jun 2026 14:58:17 +0200
Subject: [PATCH 1/3] Fix heap-buffer-overflow in LWO2 variable-sized index
 reader

ReadVSizedIntLWO2() advanced the file cursor by 2-4 bytes without ever
checking it against the end of the file buffer. A malformed LWO2 file
whose polygon index count runs past the end of the chunk makes the
reader walk off the heap allocation, producing an out-of-bounds read.
The unchecked helper is reached from all of its call sites, e.g.
CountVertsAndFacesLWO2() -> ReadVSizedIntLWO2().

Guard every byte read against mFileBufferEnd and raise a
DeadlyImportError on truncated input. Well-formed files are unaffected;
malformed files now fail cleanly instead of reading out of bounds.

Found with AddressSanitizer + libFuzzer.

Assisted-by: Claude (Anthropic)
---
 code/AssetLib/LWO/LWOLoader.h | 30 ++++++++++++++++--------------
 1 file changed, 16 insertions(+), 14 deletions(-)

diff --git a/code/AssetLib/LWO/LWOLoader.h b/code/AssetLib/LWO/LWOLoader.h
index ac6f2aa44..dd2d1dc67 100644
--- a/code/AssetLib/LWO/LWOLoader.h
+++ b/code/AssetLib/LWO/LWOLoader.h
@@ -46,6 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include "LWOFileData.h"
 #include <assimp/BaseImporter.h>
+#include <assimp/Exceptional.h>
 #include <assimp/material.h>
 #include <assimp/DefaultLogger.hpp>
 
@@ -453,24 +454,25 @@ inline uint8_t LWOImporter::GetU1() {
 
 // ------------------------------------------------------------------------------------------------
 inline int LWOImporter::ReadVSizedIntLWO2(uint8_t *&inout) {
+    // A variable-sized index is 2 bytes long, or 4 bytes when the first byte
+    // is 0xFF. Guard every byte read against the end of the file buffer so a
+    // truncated or malformed chunk cannot trigger an out-of-bounds read.
+    auto readByte = [&]() -> int {
+        if (inout >= mFileBufferEnd) {
+            throw DeadlyImportError("LWO2: Unexpected end of file while reading a variable-sized index");
+        }
+        return *inout++;
+    };
+
     int i;
-    int c = *inout;
-    inout++;
+    int c = readByte();
     if (c != 0xFF) {
         i = c << 8;
-        c = *inout;
-        inout++;
-        i |= c;
+        i |= readByte();
     } else {
-        c = *inout;
-        inout++;
-        i = c << 16;
-        c = *inout;
-        inout++;
-        i |= c << 8;
-        c = *inout;
-        inout++;
-        i |= c;
+        i = readByte() << 16;
+        i |= readByte() << 8;
+        i |= readByte();
     }
     return i;
 }

From 0f6bcfe7acd4c16bc198560db1be848757f953a8 Mon Sep 17 00:00:00 2001
From: Kim Kulling <kimkulling@users.noreply.github.com>
Date: Wed, 8 Jul 2026 21:23:39 +0200
Subject: [PATCH 2/3] Refactor comments in LWOLoader.h

Removed unnecessary comments and added brief descriptions to function declarations.
---
 code/AssetLib/LWO/LWOLoader.h | 200 +++++++++++++---------------------
 1 file changed, 73 insertions(+), 127 deletions(-)

diff --git a/code/AssetLib/LWO/LWOLoader.h b/code/AssetLib/LWO/LWOLoader.h
index dd2d1dc67..8431edd28 100644
--- a/code/AssetLib/LWO/LWOLoader.h
+++ b/code/AssetLib/LWO/LWOLoader.h
@@ -72,28 +72,10 @@ using namespace LWO;
 // ---------------------------------------------------------------------------
 class LWOImporter final : public BaseImporter {
 public:
-    /**
-     * @brief The class constructor.
-     */
     LWOImporter() = default;
-
-    /**
-     * @brief The class destructor.
-     */
     ~LWOImporter() override = default;
-
-    // -------------------------------------------------------------------
-    /** Returns whether the class can handle the format of the given file.
-     * See BaseImporter::CanRead() for details.
-     */
     bool CanRead(const std::string &pFile, IOSystem *pIOHandler,
             bool checkSig) const override;
-
-    // -------------------------------------------------------------------
-    /** Called prior to ReadFile().
-    * The function is a request to the importer to update its configuration
-    * basing on the Importer's configuration property list.
-    */
     void SetupProperties(const Importer *pImp) override;
 
 protected:
@@ -110,18 +92,15 @@ protected:
 
 private:
     // -------------------------------------------------------------------
-    /** Loads a LWO file in the older LWOB format (LW < 6)
-     */
+    /// Loads a LWO file in the older LWOB format (LW < 6)
     void LoadLWOBFile();
 
     // -------------------------------------------------------------------
-    /** Loads a LWO file in the newer LWO2 format (LW >= 6)
-     */
+    /// Loads a LWO file in the newer LWO2 format (LW >= 6)
     void LoadLWO2File();
 
     // -------------------------------------------------------------------
-    /** Parsing functions used for all file format versions
-    */
+    /// Parsing functions used for all file format versions
     void GetS0(std::string &out, unsigned int max);
     float GetF4();
     float GetF8();
@@ -131,39 +110,34 @@ private:
     uint8_t GetU1();
 
     // -------------------------------------------------------------------
-    /** Loads a surface chunk from an LWOB file
-     *  @param size Maximum size to be read, in bytes.
-     */
+    /// Loads a surface chunk from an LWOB file
+    /// @param size Maximum size to be read, in bytes.
     void LoadLWOBSurface(unsigned int size);
 
     // -------------------------------------------------------------------
-    /** Loads a surface chunk from an LWO2 file
-     *  @param size Maximum size to be read, in bytes.
-     */
+    /// Loads a surface chunk from an LWO2 file
+    ///  @param size Maximum size to be read, in bytes.
     void LoadLWO2Surface(unsigned int size);
     void LoadLWO3Surface(unsigned int size);
 
     // -------------------------------------------------------------------
-    /** Loads a texture block from a LWO2 file.
-     *  @param size Maximum size to be read, in bytes.
-     *  @param head Header of the SUF.BLOK header
-     */
+    /// Loads a texture block from a LWO2 file.
+    /// @param size Maximum size to be read, in bytes.
+    /// @param head Header of the SUF.BLOK header
     void LoadLWO2TextureBlock(LE_NCONST IFF::SubChunkHeader *head,
             unsigned int size);
 
     // -------------------------------------------------------------------
-    /** Loads a shader block from a LWO2 file.
-     *  @param size Maximum size to be read, in bytes.
-     *  @param head Header of the SUF.BLOK header
-     */
+    /// Loads a shader block from a LWO2 file.
+    /// @param size Maximum size to be read, in bytes.
+    /// @param head Header of the SUF.BLOK header
     void LoadLWO2ShaderBlock(LE_NCONST IFF::SubChunkHeader *head,
             unsigned int size);
 
     // -------------------------------------------------------------------
-    /** Loads an image map from a LWO2 file
-     *  @param size Maximum size to be read, in bytes.
-     *  @param tex Texture object to be filled
-     */
+    /// Loads an image map from a LWO2 file
+    /// @param size Maximum size to be read, in bytes.
+    /// @param tex Texture object to be filled
     void LoadLWO2ImageMap(unsigned int size, LWO::Texture &tex);
     void LoadLWO2Gradient(unsigned int size, LWO::Texture &tex);
     void LoadLWO2Procedural(unsigned int size, LWO::Texture &tex);
@@ -172,48 +146,41 @@ private:
     void LoadLWO2TextureHeader(unsigned int size, LWO::Texture &tex);
 
     // -------------------------------------------------------------------
-    /** Loads the LWO tag list from the file
-     *  @param size Maximum size to be read, in bytes.
-     */
+    /// Loads the LWO tag list from the file
+    /// @param size Maximum size to be read, in bytes.
     void LoadLWOTags(unsigned int size);
 
     // -------------------------------------------------------------------
-    /** Load polygons from a POLS chunk
-     *  @param length Size of the chunk
-    */
+    /// Load polygons from a POLS chunk
+    /// @param length Size of the chunk
     void LoadLWO2Polygons(unsigned int length);
     void LoadLWOBPolygons(unsigned int length);
 
     // -------------------------------------------------------------------
-    /** Load polygon tags from a PTAG chunk
-     *  @param length Size of the chunk
-    */
+    /// Load polygon tags from a PTAG chunk
+    /// @param length Size of the chunk
     void LoadLWO2PolygonTags(unsigned int length);
 
     // -------------------------------------------------------------------
-    /** Load a vertex map from a VMAP/VMAD chunk
-     *  @param length Size of the chunk
-     *  @param perPoly Operate on per-polygon base?
-    */
+    /// Load a vertex map from a VMAP/VMAD chunk
+    /// @param length Size of the chunk
+    /// @param perPoly Operate on per-polygon base?
     void LoadLWO2VertexMap(unsigned int length, bool perPoly);
 
     // -------------------------------------------------------------------
-    /** Load polygons from a PNTS chunk
-     *  @param length Size of the chunk
-    */
+    /// Load polygons from a PNTS chunk
+    /// @param length Size of the chunk
     void LoadLWOPoints(unsigned int length);
 
     // -------------------------------------------------------------------
-    /** Load a clip from a CLIP chunk
-     *  @param length Size of the chunk
-    */
+    /// Load a clip from a CLIP chunk
+    /// @param length Size of the chunk
     void LoadLWO2Clip(unsigned int length);
     void LoadLWO3Clip(unsigned int length);
 
     // -------------------------------------------------------------------
-    /** Load an envelope from an EVL chunk
-     *  @param length Size of the chunk
-    */
+    /// Load an envelope from an EVL chunk
+    /// @param length Size of the chunk
     void LoadLWO2Envelope(unsigned int length);
     void LoadLWO3Envelope(unsigned int length);
 
@@ -227,8 +194,7 @@ private:
     void LoadNodeData(unsigned int length);
 
     // -------------------------------------------------------------------
-    /** Count vertices and faces in a LWOB/LWO2 file
-    */
+    /// Count vertices and faces in a LWOB/LWO2 file
     void CountVertsAndFacesLWO2(unsigned int &verts,
             unsigned int &faces,
             uint16_t *&cursor,
@@ -242,8 +208,7 @@ private:
             unsigned int max = UINT_MAX);
 
     // -------------------------------------------------------------------
-    /** Read vertices and faces in a LWOB/LWO2 file
-    */
+    /// Read vertices and faces in a LWOB/LWO2 file
     void CopyFaceIndicesLWO2(LWO::FaceList::iterator &it,
             uint16_t *&cursor,
             const uint16_t *const end);
@@ -255,45 +220,36 @@ private:
             unsigned int max = UINT_MAX);
 
     // -------------------------------------------------------------------
-    /** Resolve the tag and surface lists that have been loaded.
-    *   Generates the mMapping table.
-    */
+    /// Resolve the tag and surface lists that have been loaded.
+    /// Generates the mMapping table.
     void ResolveTags();
 
     // -------------------------------------------------------------------
-    /** Resolve the clip list that has been loaded.
-    *   Replaces clip references with real clips.
-    */
+    /// Resolve the clip list that has been loaded.
+    /// Replaces clip references with real clips.
     void ResolveClips();
 
     // -------------------------------------------------------------------
-    /** Add a texture list to an output material description.
-     *
-     *  @param pcMat Output material
-     *  @param in Input texture list
-     *  @param type Type identifier of the texture list
-    */
+    /// Add a texture list to an output material description.
+    /// @param pcMat Output material
+    /// @param in Input texture list
+    /// @param type Type identifier of the texture list
     bool HandleTextures(aiMaterial *pcMat, const TextureList &in,
             aiTextureType type);
 
     // -------------------------------------------------------------------
-    /** Adjust a texture path
-    */
+    /// Adjust a texture path
     void AdjustTexturePath(std::string &out);
 
     // -------------------------------------------------------------------
-    /** Convert a LWO surface description to an ASSIMP material
-    */
+    /// Convert a LWO surface description to an ASSIMP material
     void ConvertMaterial(const LWO::Surface &surf, aiMaterial *pcMat);
 
     // -------------------------------------------------------------------
-    /** Get a list of all UV/VC channels required by a specific surface.
-     *
-     *  @param surf Working surface
-     *  @param layer Working layer
-     *  @param out Output list. The members are indices into the
-     *    UV/VC channel lists of the layer
-    */
+    /// Get a list of all UV/VC channels required by a specific surface.
+    /// @param surf Working surface
+    /// @param layer Working layer
+    /// @param out Output list. The members are indices into the UV/VC channel lists of the layer
     void FindUVChannels(/*const*/ LWO::Surface &surf,
             LWO::SortedRep &sorted,
             /*const*/ LWO::Layer &layer,
@@ -310,57 +266,48 @@ private:
             unsigned int out[AI_MAX_NUMBER_OF_COLOR_SETS]);
 
     // -------------------------------------------------------------------
-    /** Generate the final node graph
-     *  Unused nodes are deleted.
-     *  @param apcNodes Flat list of nodes
-    */
+    /// Generate the final node graph
+    /// Unused nodes are deleted.
+    /// @param apcNodes Flat list of nodes
     void GenerateNodeGraph(std::map<uint16_t, aiNode *> &apcNodes);
 
     // -------------------------------------------------------------------
-    /** Add children to a node
-     *  @param node Node to become a father
-     *  @param parent Index of the node
-     *  @param apcNodes Flat list of nodes - used nodes are set to nullptr.
-    */
-    void AddChildren(aiNode *node, uint16_t parent,
-            std::vector<aiNode *> &apcNodes);
+    /// Add children to a node
+    /// @param node Node to become a father
+    /// @param parent Index of the node
+    /// @param apcNodes Flat list of nodes - used nodes are set to nullptr.
+    void AddChildren(aiNode *node, uint16_t parent, std::vector<aiNode *> &apcNodes);
 
     // -------------------------------------------------------------------
-    /** Read a variable sized integer
-     *  @param inout Input and output buffer
-    */
+    /// Read a variable sized integer
+    /// @param inout Input and output buffer
     int ReadVSizedIntLWO2(uint8_t *&inout);
 
     // -------------------------------------------------------------------
-    /** Assign a value from a VMAP to a vertex and all vertices
-     *  attached to it.
-     *  @param base VMAP destination data
-     *  @param numRead Number of float's to be read
-     *  @param idx Absolute index of the first vertex
-     *  @param data Value of the VMAP to be assigned - read numRead
-     *    floats from this array.
+    /// Assign a value from a VMAP to a vertex and all vertices attached to it.
+    /// @param base VMAP destination data
+    /// @param numRead Number of float's to be read
+    /// @param idx Absolute index of the first vertex
+    /// @param data Value of the VMAP to be assigned - read numRead
+    ///        floats from this array.
     */
     void DoRecursiveVMAPAssignment(VMapEntry *base, unsigned int numRead,
             unsigned int idx, float *data);
 
     // -------------------------------------------------------------------
-    /** Compute normal vectors for a mesh
-     *  @param mesh Input mesh
-     *  @param smoothingGroups Smoothing-groups-per-face array
-     *  @param surface Surface for the mesh
-    */
+    /// Compute normal vectors for a mesh
+    /// @param mesh Input mesh
+    /// @param smoothingGroups Smoothing-groups-per-face array
+    /// @param surface Surface for the mesh
     void ComputeNormals(aiMesh *mesh, const std::vector<unsigned int> &smoothingGroups,
             const LWO::Surface &surface);
 
     // -------------------------------------------------------------------
-    /** Setup a new texture after the corresponding chunk was
-     *  encountered in the file.
-     *  @param list Texture list
-     *  @param size Maximum number of bytes to be read
-     *  @return Pointer to new texture
-    */
-    LWO::Texture *SetupNewTextureLWOB(LWO::TextureList &list,
-            unsigned int size);
+    /// Setup a new texture after the corresponding chunk was encountered in the file.
+    /// @param list Texture list
+    /// @param size Maximum number of bytes to be read
+    /// @return Pointer to new texture
+    LWO::Texture *SetupNewTextureLWOB(LWO::TextureList &list, unsigned int size);
 
 private:
     /// true if the file is a LWO2 file
@@ -465,8 +412,7 @@ inline int LWOImporter::ReadVSizedIntLWO2(uint8_t *&inout) {
     };
 
     int i;
-    int c = readByte();
-    if (c != 0xFF) {
+    if (int c = readByte(); c != 0xFF) {
         i = c << 8;
         i |= readByte();
     } else {

From 924bb602e387e10ca7c64acaac83a26d6ad1d8c6 Mon Sep 17 00:00:00 2001
From: Alb3e3 <74142887+Alb3e3@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:03:57 +0200
Subject: [PATCH 3/3] LWO: remove stray comment terminator in LWOLoader.h

The comment refactor left a dangling '*/' after the DoRecursiveVMAPAssignment
doc block was converted from /** ... */ to /// line comments. A bare '*/' in
code is a syntax error (expected unqualified-id before '/' token) and broke
the build on every platform. Remove the orphaned terminator.
---
 code/AssetLib/LWO/LWOLoader.h | 1 -
 1 file changed, 1 deletion(-)

diff --git a/code/AssetLib/LWO/LWOLoader.h b/code/AssetLib/LWO/LWOLoader.h
index 8431edd28..aaf205434 100644
--- a/code/AssetLib/LWO/LWOLoader.h
+++ b/code/AssetLib/LWO/LWOLoader.h
@@ -290,7 +290,6 @@ private:
     /// @param idx Absolute index of the first vertex
     /// @param data Value of the VMAP to be assigned - read numRead
     ///        floats from this array.
-    */
     void DoRecursiveVMAPAssignment(VMapEntry *base, unsigned int numRead,
             unsigned int idx, float *data);