Commit 2b2f9610 for openh264

commit 2b2f96108c73efc887a5c91a07e23ecacfc3e0f9
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date:   Mon Aug 31 11:26:30 2026 +0800

    decoder: retarget parse-only pNalPos after saved-buffer growth (#3992)

    In parse-only mode, queued AU entries cache sVclNal.pNalPos pointers into sSavedData. ExpandBsBuffer reallocated and freed sSavedData but did not retarget cached pNalPos, allowing DecodeFrameConstruction to memcpy from freed memory (UAF read).

    Retarget pNalPos to the new saved-buffer base using preserved offsets before freeing the old allocation. Add a focused decoder UT that validates pNalPos is rebased correctly after ExpandBsBuffer in parse-only mode.

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

diff --git a/codec/decoder/core/src/decoder_core.cpp b/codec/decoder/core/src/decoder_core.cpp
index 3bde051c..eb73c456 100644
--- a/codec/decoder/core/src/decoder_core.cpp
+++ b/codec/decoder/core/src/decoder_core.cpp
@@ -686,6 +686,7 @@ int32_t ExpandBsBuffer (PWelsDecoderContext pCtx, const int kiSrcLen) {

   if (pCtx->pParam->bParseOnly) {
     //Realloc sSavedData
+    uint8_t* pOldSavedBsBuff = pCtx->sSavedData.pHead;
     uint8_t* pNewSavedBsBuff = static_cast<uint8_t*> (pMa->WelsMallocz (iNewBuffLen, "pCtx->sSavedData.pHead"));
     if (pNewSavedBsBuff == NULL) {
       WelsLog (& (pCtx->sLogCtx), WELS_LOG_ERROR, "ExpandBsBuffer() Failed for malloc pNewSavedBsBuff (%d)", iNewBuffLen);
@@ -698,6 +699,21 @@ int32_t ExpandBsBuffer (PWelsDecoderContext pCtx, const int kiSrcLen) {
     pCtx->sSavedData.pStartPos = pNewSavedBsBuff + (pCtx->sSavedData.pStartPos - pCtx->sSavedData.pHead);
     pCtx->sSavedData.pCurPos   = pNewSavedBsBuff + (pCtx->sSavedData.pCurPos   - pCtx->sSavedData.pHead);
     pCtx->sSavedData.pEnd      = pNewSavedBsBuff + iNewBuffLen;
+
+    // Retarget pNalPos for all queued NALs (current AU + next AU) before freeing.
+    const uintptr_t kuiOldStart = reinterpret_cast<uintptr_t> (pOldSavedBsBuff);
+    const uintptr_t kuiOldEnd = kuiOldStart + static_cast<uintptr_t> (pCtx->iMaxBsBufferSizeInByte);
+    for (uint32_t i = 0; i < kuiRetargetNum; ++i) {
+      PNalUnit pNal = pCtx->pAccessUnitList->pNalUnitsList[i];
+      if (pNal == NULL || pNal->sNalData.sVclNal.pNalPos == NULL) {
+        continue;
+      }
+      const uintptr_t kuiNalPos = reinterpret_cast<uintptr_t> (pNal->sNalData.sVclNal.pNalPos);
+      if (kuiNalPos >= kuiOldStart && kuiNalPos < kuiOldEnd) {
+        pNal->sNalData.sVclNal.pNalPos = pNewSavedBsBuff + (kuiNalPos - kuiOldStart);
+      }
+    }
+
     pMa->WelsFree (pCtx->sSavedData.pHead, "pCtx->sSavedData.pHead");
     pCtx->sSavedData.pHead = pNewSavedBsBuff;
   }
diff --git a/test/decoder/DecUT_ParseSyntax.cpp b/test/decoder/DecUT_ParseSyntax.cpp
index 694e1a45..93de13d7 100644
--- a/test/decoder/DecUT_ParseSyntax.cpp
+++ b/test/decoder/DecUT_ParseSyntax.cpp
@@ -136,7 +136,7 @@ class DecoderParseSyntaxTest : public ::testing::Test {
     }
   }
   //Init members
-  int32_t Init();
+  int32_t Init (bool bParseOnly = false);
   //Uninit members
   void Uninit();
   //Decoder real bitstream
@@ -174,7 +174,7 @@ class DecoderParseSyntaxTest : public ::testing::Test {
 };

 //Init members
-int32_t DecoderParseSyntaxTest::Init() {
+int32_t DecoderParseSyntaxTest::Init (bool bParseOnly) {
   memset (&m_sBufferInfo, 0, sizeof (SBufferInfo));
   memset (&m_sDecParam, 0, sizeof (SDecodingParam));
   memset (&m_sParserBsInfo, 0, sizeof (SParserBsInfo));
@@ -189,7 +189,7 @@ int32_t DecoderParseSyntaxTest::Init() {
   m_sDecParam.eEcActiveIdc = (ERROR_CON_IDC)7;
   m_sDecParam.sVideoProperty.size = sizeof (SVideoProperty);
   m_sDecParam.sVideoProperty.eVideoBsType = (VIDEO_BITSTREAM_TYPE) (rand() % 2);
-  m_sDecParam.bParseOnly = false;
+  m_sDecParam.bParseOnly = bParseOnly;

   m_pData[0] = m_pData[1] = m_pData[2] = NULL;
   m_szBuffer[0] = m_szBuffer[1] = m_szBuffer[2] = 0;
@@ -512,6 +512,48 @@ void DecoderParseSyntaxTest::TestIPcmCavlcTruncated() {
   Uninit();
 }

+// Verify that ExpandBsBuffer retargets pNalPos for both current-AU and queued
+// next-AU NALs; pre-fix, only [0, uiActualUnitsNum] was walked, leaving
+// next-AU entries dangling into the freed sSavedData buffer.
+TEST_F (DecoderParseSyntaxTest, ExpandBsBufferRetargetsParseOnlyNalPos) {
+  ASSERT_EQ (cmResultSuccess, Init (true));
+
+  ASSERT_TRUE (m_pCtx != NULL);
+  ASSERT_TRUE (m_pCtx->pParam != NULL);
+  ASSERT_TRUE (m_pCtx->pParam->bParseOnly);
+  ASSERT_TRUE (m_pCtx->sSavedData.pHead != NULL);
+  ASSERT_TRUE (m_pCtx->pAccessUnitList != NULL);
+  ASSERT_TRUE (m_pCtx->pAccessUnitList->pNalUnitsList != NULL);
+  ASSERT_TRUE (m_pCtx->pAccessUnitList->uiCountUnitsNum >= 2);
+
+  // Slot 0: current-AU NAL; slot 1: queued next-AU NAL (uiAvailUnitsNum > uiActualUnitsNum).
+  m_pCtx->pAccessUnitList->uiActualUnitsNum = 1;
+  m_pCtx->pAccessUnitList->uiAvailUnitsNum  = 2;
+
+  uint8_t* pOldSavedHead = m_pCtx->sSavedData.pHead;
+  const int32_t kiOldSavedSize = m_pCtx->iMaxBsBufferSizeInByte;
+  ASSERT_TRUE (kiOldSavedSize > 64);
+
+  PNalUnit pNal0 = m_pCtx->pAccessUnitList->pNalUnitsList[0];
+  PNalUnit pNal1 = m_pCtx->pAccessUnitList->pNalUnitsList[1];
+  ASSERT_TRUE (pNal0 != NULL);
+  ASSERT_TRUE (pNal1 != NULL);
+
+  pNal0->sNalData.sVclNal.pNalPos = pOldSavedHead + 16;
+  pNal1->sNalData.sVclNal.pNalPos = pOldSavedHead + 32;  // queued next-AU entry
+
+  const int32_t kiSrcLen = kiOldSavedSize / MAX_BUFFERED_NUM + 1;
+  ASSERT_EQ (ERR_NONE, ExpandBsBuffer (m_pCtx, kiSrcLen));
+
+  ASSERT_TRUE (m_pCtx->sSavedData.pHead != NULL);
+  EXPECT_NE (pOldSavedHead, m_pCtx->sSavedData.pHead);
+  // Both current-AU and queued next-AU pNalPos must be retargeted.
+  EXPECT_EQ (m_pCtx->sSavedData.pHead + 16, pNal0->sNalData.sVclNal.pNalPos);
+  EXPECT_EQ (m_pCtx->sSavedData.pHead + 32, pNal1->sNalData.sVclNal.pNalPos);
+
+  Uninit();
+}
+
 //TEST here for whole tests
 TEST_F (DecoderParseSyntaxTest, DecoderParseSyntaxTestAll) {