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];