Commit d59cefb5 for openh264

commit d59cefb549984422fe293957b579ce41d2b3dd4a
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date:   Fri Jul 24 14:27:29 2026 +0800

    decoder: validate FMO slice-group run length before building allocation map (#3967)

    * decoder: validate FMO slice-group run length before building allocation map

    * decoder: add UT for FMO type0 run length guard

    * decoder: free FMO allocation map on rejected slice-group run length

    The run-length validation added for SPARK-814291 makes FmoGenerateMbAllocMapType0 return an error, but FmoGenerateSliceGroup had already allocated pFmo->pMbAllocMap. On that error path FmoParamUpdate returns before setting bActiveFlag, so UninitFmoList never frees the map -> memory leak (caught by CIFuzz: CMemoryAlign m_nMemoryUsageInBytes==0 assertion). Free and reset the map on the failure path, and assert it in the UT.

    ---------

    Co-authored-by: benzzhan <benzzhan@cisco.com>

diff --git a/codec/decoder/core/src/au_parser.cpp b/codec/decoder/core/src/au_parser.cpp
index 63a882e1..d6b0570c 100644
--- a/codec/decoder/core/src/au_parser.cpp
+++ b/codec/decoder/core/src/au_parser.cpp
@@ -1388,6 +1388,8 @@ int32_t ParsePps (PWelsDecoderContext pCtx, PPps pPpsList, PBitStringAux pBsAux,
     case 0:
       for (iTmp = 0; iTmp < pPps->uiNumSliceGroups; iTmp++) {
         WELS_READ_VERIFY (BsGetUe (pBsAux, &uiCode)); //run_length_minus1[ iGroup ]
+        WELS_CHECK_SE_UPPER_ERROR (uiCode, MAX_MB_SIZE - 1, "run_length_minus1",
+                                   GENERATE_ERROR_NO (ERR_LEVEL_PARAM_SETS, ERR_INFO_INVALID_SLICEGROUP));
         pPps->uiRunLength[iTmp] = RUN_LENGTH_OFFSET + uiCode;
       }
       break;
diff --git a/codec/decoder/core/src/fmo.cpp b/codec/decoder/core/src/fmo.cpp
index efaa1f11..14a6bcba 100644
--- a/codec/decoder/core/src/fmo.cpp
+++ b/codec/decoder/core/src/fmo.cpp
@@ -66,13 +66,18 @@ static inline int32_t FmoGenerateMbAllocMapType0 (PFmo pFmo, PPps pPps) {
   do {
     uint8_t uiGroup = 0;
     do {
-      const int32_t kiRunIdx = pPps->uiRunLength[uiGroup];
+      const uint32_t kuiRunIdx = pPps->uiRunLength[uiGroup];
+      // Reject zero or oversized run lengths before advancing i. A single run
+      // cannot legitimately exceed the total MB count; this also prevents any
+      // integer wrap of the map index with attacker-influenced values.
+      WELS_VERIFY_RETURN_IF (ERR_INFO_INVALID_PARAM,
+                 (0 == kuiRunIdx || kuiRunIdx > static_cast<uint32_t> (iMbNum)))
       int32_t j = 0;
       do {
         pFmo->pMbAllocMap[i + j] = uiGroup;
         ++ j;
-      } while (j < kiRunIdx && i + j < iMbNum);
-      i += kiRunIdx;
+      } while (j < static_cast<int32_t> (kuiRunIdx) && i + j < iMbNum);
+      i += static_cast<int32_t> (kuiRunIdx);
       ++ uiGroup;
     } while (uiGroup < uiNumSliceGroups && i < iMbNum);
   } while (i < iMbNum);
@@ -170,6 +175,13 @@ static inline int32_t FmoGenerateSliceGroup (PFmo pFmo, const PPps kpPps, const
   if (0 == iErr) {      // well now
     pFmo->iSliceGroupCount = kpPps->uiNumSliceGroups;
     pFmo->iSliceGroupType  = kpPps->uiSliceGroupMapType;
+  } else {
+    // Map generation failed (e.g. a rejected/oversized run length). The map was
+    // allocated above but this FMO is not marked active, so UninitFmoList would
+    // never free it. Release it here to avoid a leak on the rejection path.
+    pMa->WelsFree (pFmo->pMbAllocMap, "_fmo->pMbAllocMap");
+    pFmo->pMbAllocMap = NULL;
+    pFmo->iCountMbNum = 0;
   }

   return iErr;
diff --git a/test/decoder/DecUT_ParseSyntax.cpp b/test/decoder/DecUT_ParseSyntax.cpp
index 8ad21665..96e2ccc0 100644
--- a/test/decoder/DecUT_ParseSyntax.cpp
+++ b/test/decoder/DecUT_ParseSyntax.cpp
@@ -4,6 +4,7 @@
 #include "decoder_context.h"
 #include "decoder.h"
 #include "decoder_core.h"
+#include "fmo.h"
 #include "welsCodecTrace.h"
 #include "../../common/src/welsCodecTrace.cpp"

@@ -442,6 +443,32 @@ TEST_F (DecoderParseSyntaxTest, DecoderParseSyntaxTestAll) {
   TestSpecificBsError();
 }

+TEST (DecoderFmoSecurityTest, RejectsOversizedRunLengthBeforeIndexWrap) {
+  SFmo sFmo;
+  SPps sPps;
+  memset (&sFmo, 0, sizeof (sFmo));
+  memset (&sPps, 0, sizeof (sPps));
+
+  sPps.uiNumSliceGroups = 2;
+  sPps.uiSliceGroupMapType = 0;
+  sPps.uiRunLength[0] = 0xffffffffu;
+  sPps.uiRunLength[1] = 1;
+
+  CMemoryAlign cMa (16);
+  const int32_t iRet = InitFmo (&sFmo, &sPps, 120, 68, &cMa);
+  EXPECT_NE (ERR_NONE, iRet);
+
+  // The rejection path must also free the allocation map it allocated, otherwise
+  // the FMO is left allocated-but-inactive and leaks (CMemoryAlign asserts on
+  // teardown). A non-NULL map here means the leak regressed.
+  EXPECT_TRUE (NULL == sFmo.pMbAllocMap);
+
+  if (NULL != sFmo.pMbAllocMap) {
+    cMa.WelsFree (sFmo.pMbAllocMap, "_fmo->pMbAllocMap");
+    sFmo.pMbAllocMap = NULL;
+  }
+}
+
 TEST (DecoderReorderingBufferTest, PartialResetInitializesPicBuffIdx) {
   SPictReoderingStatus sStatus;
   SPictInfo sPictInfo[16];