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);
+}