Commit 4a2615fa for openh264
commit 4a2615fac570c6ca1ed4f157b9fdab9466edfd80
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date: Fri Jul 24 15:35:17 2026 +0800
decoder: bounds-check CAVLC I_PCM copy before reading bitstream (#3968)
* decoder: bounds-check CAVLC I_PCM copy before reading bitstream
* decoder: replace I_PCM 384 magic number with I_PCM_MB_SIZE_IN_BYTE
Define I_PCM_MB_SIZE_IN_BYTE (16*16 luma + 2*(8*8) chroma) in decode_slice.h and use it at the three CAVLC I_PCM sites and the CABAC ParseIPCMInfoCabac sibling (bounds check + pCurBuf advance). Value is unchanged (384); no functional change.
---------
Co-authored-by: benzzhan <benzzhan@cisco.com>
diff --git a/codec/decoder/core/inc/decode_slice.h b/codec/decoder/core/inc/decode_slice.h
index c89e6fe7..18791f4c 100644
--- a/codec/decoder/core/inc/decode_slice.h
+++ b/codec/decoder/core/inc/decode_slice.h
@@ -37,6 +37,10 @@
namespace WelsDec {
+// Raw byte size of one 4:2:0 8-bit I_PCM macroblock copied verbatim from the
+// bitstream: 16x16 luma + 2 x (8x8) chroma.
+#define I_PCM_MB_SIZE_IN_BYTE (16 * 16 + 2 * (8 * 8))
+
int32_t WelsActualDecodeMbCavlcISlice (PWelsDecoderContext pCtx);
int32_t WelsDecodeMbCavlcISlice (PWelsDecoderContext pCtx, PNalUnit pNalCur, uint32_t& uiEosFlag);
diff --git a/codec/decoder/core/src/decode_slice.cpp b/codec/decoder/core/src/decode_slice.cpp
index b46bbeda..42de1db0 100644
--- a/codec/decoder/core/src/decode_slice.cpp
+++ b/codec/decoder/core/src/decode_slice.cpp
@@ -1845,6 +1845,13 @@ int32_t WelsActualDecodeMbCavlcISlice (PWelsDecoderContext pCtx) {
//step 1: locating bit-stream pointer [must align into integer byte]
pBs->pCurBuf -= iIndex;
+ //bounds check: I_PCM copies I_PCM_MB_SIZE_IN_BYTE (256 luma + 128 chroma) bytes
+ //directly from the bitstream buffer; reject when fewer bytes remain to avoid
+ //an out-of-bounds read (mirrors ParseIPCMInfoCabac).
+ if (pBs->pEndBuf - pBs->pCurBuf < I_PCM_MB_SIZE_IN_BYTE) {
+ return GENERATE_ERROR_NO (ERR_LEVEL_MB_DATA, ERR_INFO_BS_INCOMPLETE);
+ }
+
//step 2: copy pixel from bit-stream into fdec [reconstruction]
pTmpBsBuf = pBs->pCurBuf;
if (!pCtx->pParam->bParseOnly) {
@@ -1865,7 +1872,7 @@ int32_t WelsActualDecodeMbCavlcISlice (PWelsDecoderContext pCtx) {
}
}
- pBs->pCurBuf += 384;
+ pBs->pCurBuf += I_PCM_MB_SIZE_IN_BYTE;
//step 3: update QP and pNonZeroCount
pCurDqLayer->pLumaQp[iMbXy] = 0;
@@ -2186,6 +2193,13 @@ int32_t WelsActualDecodeMbCavlcPSlice (PWelsDecoderContext pCtx) {
//step 1: locating bit-stream pointer [must align into integer byte]
pBs->pCurBuf -= iIndex;
+ //bounds check: I_PCM copies I_PCM_MB_SIZE_IN_BYTE (256 luma + 128 chroma) bytes
+ //directly from the bitstream buffer; reject when fewer bytes remain to avoid
+ //an out-of-bounds read (mirrors ParseIPCMInfoCabac).
+ if (pBs->pEndBuf - pBs->pCurBuf < I_PCM_MB_SIZE_IN_BYTE) {
+ return GENERATE_ERROR_NO (ERR_LEVEL_MB_DATA, ERR_INFO_BS_INCOMPLETE);
+ }
+
//step 2: copy pixel from bit-stream into fdec [reconstruction]
pTmpBsBuf = pBs->pCurBuf;
if (!pCtx->pParam->bParseOnly) {
@@ -2207,7 +2221,7 @@ int32_t WelsActualDecodeMbCavlcPSlice (PWelsDecoderContext pCtx) {
}
}
- pBs->pCurBuf += 384;
+ pBs->pCurBuf += I_PCM_MB_SIZE_IN_BYTE;
//step 3: update QP and pNonZeroCount
pCurDqLayer->pLumaQp[iMbXy] = 0;
@@ -2733,6 +2747,13 @@ int32_t WelsActualDecodeMbCavlcBSlice (PWelsDecoderContext pCtx) {
//step 1: locating bit-stream pointer [must align into integer byte]
pBs->pCurBuf -= iIndex;
+ //bounds check: I_PCM copies I_PCM_MB_SIZE_IN_BYTE (256 luma + 128 chroma) bytes
+ //directly from the bitstream buffer; reject when fewer bytes remain to avoid
+ //an out-of-bounds read (mirrors ParseIPCMInfoCabac).
+ if (pBs->pEndBuf - pBs->pCurBuf < I_PCM_MB_SIZE_IN_BYTE) {
+ return GENERATE_ERROR_NO (ERR_LEVEL_MB_DATA, ERR_INFO_BS_INCOMPLETE);
+ }
+
//step 2: copy pixel from bit-stream into fdec [reconstruction]
pTmpBsBuf = pBs->pCurBuf;
if (!pCtx->pParam->bParseOnly) {
@@ -2754,7 +2775,7 @@ int32_t WelsActualDecodeMbCavlcBSlice (PWelsDecoderContext pCtx) {
}
}
- pBs->pCurBuf += 384;
+ pBs->pCurBuf += I_PCM_MB_SIZE_IN_BYTE;
//step 3: update QP and pNonZeroCount
pCurDqLayer->pLumaQp[iMbXy] = 0;
diff --git a/codec/decoder/core/src/parse_mb_syn_cabac.cpp b/codec/decoder/core/src/parse_mb_syn_cabac.cpp
index 177a5e82..4f77ec40 100644
--- a/codec/decoder/core/src/parse_mb_syn_cabac.cpp
+++ b/codec/decoder/core/src/parse_mb_syn_cabac.cpp
@@ -1523,7 +1523,7 @@ int32_t ParseIPCMInfoCabac (PWelsDecoderContext pCtx) {
pCurDqLayer->pDec->pMbType[iMbXy] = MB_TYPE_INTRA_PCM;
RestoreCabacDecEngineToBS (pCabacDecEngine, pBsAux);
intX_t iBytesLeft = pBsAux->pEndBuf - pBsAux->pCurBuf;
- if (iBytesLeft < 384) {
+ if (iBytesLeft < I_PCM_MB_SIZE_IN_BYTE) {
return GENERATE_ERROR_NO (ERR_LEVEL_MB_DATA, ERR_CABAC_NO_BS_TO_READ);
}
pPtrSrc = pBsAux->pCurBuf;
@@ -1545,7 +1545,7 @@ int32_t ParseIPCMInfoCabac (PWelsDecoderContext pCtx) {
}
}
- pBsAux->pCurBuf += 384;
+ pBsAux->pCurBuf += I_PCM_MB_SIZE_IN_BYTE;
pCurDqLayer->pLumaQp[iMbXy] = 0;
pCurDqLayer->pChromaQp[iMbXy][0] = pCurDqLayer->pChromaQp[iMbXy][1] = 0;
diff --git a/test/decoder/DecUT_ParseSyntax.cpp b/test/decoder/DecUT_ParseSyntax.cpp
index 96e2ccc0..ea2f076a 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 "error_concealment.h"
#include "fmo.h"
#include "welsCodecTrace.h"
#include "../../common/src/welsCodecTrace.cpp"
@@ -147,6 +148,9 @@ class DecoderParseSyntaxTest : public ::testing::Test {
//specific bitstream test
void TestSpecificBs();
void TestSpecificBsError();
+ //I_PCM CAVLC bounds handling
+ void TestIPcmCavlcRegression();
+ void TestIPcmCavlcTruncated();
//Do whole tests here
void DecoderParseSyntaxTestAll();
@@ -435,12 +439,87 @@ void DecoderParseSyntaxTest::TestSpecificBsError() {
Uninit();
}
+// Regression: a valid CAVLC stream that uses I_PCM macroblocks must keep
+// decoding correctly after the I_PCM 384-byte bounds check was added.
+// CVPCMNL1_SVA_C.264 is a CAVLC I_PCM conformance stream (exercises the
+// patched WelsActualDecodeMbCavlcISlice path).
+void DecoderParseSyntaxTest::TestIPcmCavlcRegression() {
+ int32_t iRet = Init();
+ ASSERT_EQ (iRet, ERR_NONE);
+ ASSERT_TRUE (DecodeBs ("res/CVPCMNL1_SVA_C.264", CorrectDec));
+ Uninit();
+}
+
+// Security regression for SPARK-814292 (CWE-125): a CAVLC I_PCM macroblock
+// copies 384 bytes straight from the bitstream buffer. When the buffer is
+// truncated mid I_PCM, the decoder must fail closed instead of reading past
+// the allocation. Feeding a truncated all-I_PCM stream must not crash and
+// must surface a decoding error rather than dsErrorFree.
+void DecoderParseSyntaxTest::TestIPcmCavlcTruncated() {
+ int32_t iRet = Init();
+ ASSERT_EQ (iRet, ERR_NONE);
+
+ // Disable error concealment so a truncated I_PCM macroblock surfaces as a
+ // bitstream error instead of being silently concealed.
+ m_pCtx->pParam->eEcActiveIdc = ERROR_CON_DISABLE;
+ InitErrorCon (m_pCtx);
+
+ FILE* pH264File = fopen ("res/CVPCMNL1_SVA_C.264", "rb");
+ ASSERT_TRUE (pH264File != NULL);
+ fseek (pH264File, 0L, SEEK_END);
+ int32_t iFileSize = (int32_t) ftell (pH264File);
+ fseek (pH264File, 0L, SEEK_SET);
+ ASSERT_GT (iFileSize, 384);
+
+ // Drop the trailing 384 bytes so the final I_PCM macroblock is short of a
+ // full 384-byte PCM payload.
+ int32_t iTruncatedSize = iFileSize - 384;
+ uint8_t* pBuf = new uint8_t[iTruncatedSize + 4];
+ ASSERT_TRUE (pBuf != NULL);
+ size_t uiRead = fread (pBuf, 1, iTruncatedSize, pH264File);
+ fclose (pH264File);
+ ASSERT_EQ (uiRead, (size_t) iTruncatedSize);
+ uint8_t uiStartCode[4] = {0, 0, 0, 1};
+ memcpy (pBuf + iTruncatedSize, &uiStartCode[0], 4);
+
+ int32_t iBufPos = 0;
+ int32_t iAggregatedRet = 0;
+ while (iBufPos < iTruncatedSize) {
+ int32_t i = 0;
+ for (i = 0; i < iTruncatedSize - iBufPos; i++) {
+ if (pBuf[iBufPos + i] == 0 && pBuf[iBufPos + i + 1] == 0 && pBuf[iBufPos + i + 2] == 0
+ && pBuf[iBufPos + i + 3] == 1 && i > 0) {
+ break;
+ }
+ }
+ int32_t iSliceSize = i;
+ if (iSliceSize <= 0)
+ break;
+ // The key property is stability: no out-of-bounds read / crash while
+ // decoding the truncated I_PCM payload.
+ iAggregatedRet |= DecodeFrame (pBuf + iBufPos, iSliceSize, m_pData, &m_sBufferInfo, m_pCtx);
+ iBufPos += iSliceSize;
+ }
+ // Flush the decoder so any delayed/buffered picture is emitted with its status.
+ int32_t iEndOfStreamFlag = 1;
+ m_pDec->SetOption (DECODER_OPTION_END_OF_STREAM, (void*)&iEndOfStreamFlag);
+ iAggregatedRet |= DecodeFrame (NULL, 0, m_pData, &m_sBufferInfo, m_pCtx);
+
+ // Truncated I_PCM must not be reported as a clean decode.
+ EXPECT_NE (dsErrorFree, iAggregatedRet);
+
+ delete[] pBuf;
+ Uninit();
+}
+
//TEST here for whole tests
TEST_F (DecoderParseSyntaxTest, DecoderParseSyntaxTestAll) {
TestScalingList();
TestSpecificBs();
TestSpecificBsError();
+ TestIPcmCavlcRegression();
+ TestIPcmCavlcTruncated();
}
TEST (DecoderFmoSecurityTest, RejectsOversizedRunLengthBeforeIndexWrap) {