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) {