Commit 9d974972 for openh264

commit 9d974972d9f1ef572b87d8d774af8f8c304ad3e1
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date:   Fri Sep 4 16:08:40 2026 +0800

    decoder: guard ResetCurrentAccessUnit against AU list counter overflow (#4000)

    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 5edf7990..c0990a07 100644
--- a/codec/decoder/core/src/decoder_core.cpp
+++ b/codec/decoder/core/src/decoder_core.cpp
@@ -1836,16 +1836,23 @@ void ResetCurrentAccessUnit (PWelsDecoderContext pCtx) {
     const uint32_t kuiActualNum = pCurAu->uiActualUnitsNum;
     // a more simpler method to do nal units list management prefered here
     const uint32_t kuiAvailNum  = pCurAu->uiAvailUnitsNum;
+    // Guard: counter mismatch after timeout-early-return can cause unsigned underflow.
+    if (kuiActualNum > kuiAvailNum) {
+      pCurAu->uiActualUnitsNum = pCurAu->uiAvailUnitsNum = 0;
+      return;
+    }
     const uint32_t kuiLeftNum   = kuiAvailNum - kuiActualNum;
+    // Guard: swap must stay within allocated list capacity.
+    const uint32_t kuiSwapLimit = (kuiAvailNum <= pCurAu->uiCountUnitsNum) ? kuiLeftNum : 0;

     // Swapping active nal unit nodes of succeeding AU with leading of list
-    while (iIdx < kuiLeftNum) {
+    while (iIdx < kuiSwapLimit) {
       PNalUnit t = pCurAu->pNalUnitsList[kuiActualNum + iIdx];
       pCurAu->pNalUnitsList[kuiActualNum + iIdx] = pCurAu->pNalUnitsList[iIdx];
       pCurAu->pNalUnitsList[iIdx] = t;
       ++ iIdx;
     }
-    pCurAu->uiActualUnitsNum = pCurAu->uiAvailUnitsNum = kuiLeftNum;
+    pCurAu->uiActualUnitsNum = pCurAu->uiAvailUnitsNum = kuiSwapLimit;
   }
 }

diff --git a/test/api/thread_decoder_test.cpp b/test/api/thread_decoder_test.cpp
index 0f148fda..852acb42 100644
--- a/test/api/thread_decoder_test.cpp
+++ b/test/api/thread_decoder_test.cpp
@@ -471,3 +471,58 @@ TEST_F (ThreadDecoderReorderQueueRaceTest, BufferedPictureQueueDrainsAllFrames)
   ASSERT_FALSE (HasFatalFailure());
   EXPECT_EQ (iDecodedFrames_, 50);
 }
+
+// Regression guard for the heap-buffer-overflow in ResetCurrentAccessUnit
+// introduced by #3983: the AU list must not overflow when the reference-wait
+// timeout fires repeatedly across many threaded decode calls.
+TEST (ThreadDecoderNalListBoundsTest, ResetAuListDoesNotOverflowOnRepeatedTimeouts) {
+  ISVCDecoder* dec = nullptr;
+  ASSERT_EQ (0, WelsCreateDecoder (&dec));
+  ASSERT_NE (dec, nullptr);
+
+  SDecodingParam p;
+  memset (&p, 0, sizeof (p));
+  p.uiTargetDqLayer  = UCHAR_MAX;
+  p.eEcActiveIdc     = ERROR_CON_SLICE_COPY;
+  p.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_DEFAULT;
+  int32_t iThreads = 2;
+  dec->SetOption (DECODER_OPTION_NUM_OF_THREADS, &iThreads);
+  ASSERT_EQ (0, dec->Initialize (&p));
+
+  // Read a B-frame stream whose AU accumulation at iter 13 triggers the OOB
+  // on unfixed builds (confirmed by ASAN bisect).
+  std::ifstream ifs ("res/BA_MW_D.264", std::ios::binary);
+  ASSERT_TRUE (ifs.is_open());
+  std::vector<uint8_t> bs ((std::istreambuf_iterator<char> (ifs)), {});
+  ASSERT_FALSE (bs.empty());
+
+  const size_t kChunk = 1200;
+  // 20 iterations comfortably past the iter-13 OOB threshold.
+  const int kIters = 20;
+  // dst must outlive the loop body: worker threads write ppDst asynchronously.
+  uint8_t* dst[3] = {nullptr, nullptr, nullptr};
+  for (int i = 0; i < kIters && static_cast<size_t> (i) * kChunk < bs.size(); ++i) {
+    SBufferInfo info;
+    memset (&info, 0, sizeof (info));
+    int32_t len = static_cast<int32_t> (std::min (kChunk, bs.size() - static_cast<size_t> (i) * kChunk));
+    DECODING_STATE rv = dec->DecodeFrameNoDelay (bs.data() + static_cast<size_t> (i) * kChunk, len, dst, &info);
+    // Non-fatal error codes (ref lost, EC) are acceptable; fatal errors are not.
+    const int32_t kFatalMask = dsInvalidArgument | dsInitialOptExpected | dsOutOfMemory | dsDstBufNeedExpan;
+    EXPECT_EQ (0, static_cast<int32_t> (rv) & kFatalMask) << "fatal decode error at iter " << i;
+  }
+
+  // Drain buffered frames before teardown to avoid worker-thread use-after-free.
+  int32_t iEos = 1;
+  dec->SetOption (DECODER_OPTION_END_OF_STREAM, &iEos);
+  int32_t iRemaining = 0;
+  dec->GetOption (DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, &iRemaining);
+  for (int32_t i = 0; i < iRemaining; ++i) {
+    uint8_t* dst[3] = {nullptr, nullptr, nullptr};
+    SBufferInfo info;
+    memset (&info, 0, sizeof (info));
+    dec->FlushFrame (dst, &info);
+  }
+
+  dec->Uninitialize();
+  WelsDestroyDecoder (dec);
+}