Commit cd7a487e for openh264
commit cd7a487e6b47b5d86b49bc591090cdc1770c6b75
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date: Fri Sep 4 13:21:48 2026 +0800
decoder: re-sync threaded reference snapshot after MMCO5 reset (#3999)
WelsResetRefPic clears the persistent reference list, but threaded marking operates on a temporary snapshot that is later published to the successor context. Re-sync that active snapshot after MMCO5 reset so stale picture pointers cannot survive. Add a targeted regression test covering the threaded snapshot path.
Co-authored-by: benzzhan <benzzhan@cisco.com>
diff --git a/codec/decoder/core/src/manage_dec_ref.cpp b/codec/decoder/core/src/manage_dec_ref.cpp
index 90f79409..c164272f 100644
--- a/codec/decoder/core/src/manage_dec_ref.cpp
+++ b/codec/decoder/core/src/manage_dec_ref.cpp
@@ -735,6 +735,19 @@ static int32_t MMCOProcess (PWelsDecoderContext pCtx, PRefPic pRefPic, uint32_t
break;
case MMCO_RESET:
WelsResetRefPic (pCtx);
+ if (pRefPic != &pCtx->sRefPic) {
+ // WelsResetRefPic() hard-codes pCtx->sRefPic and does not
+ // touch the caller's active reference list. In the threaded predecessor
+ // handoff path (decoder_core.cpp), pRefPic points at pCtx->sTmpRefPic, a
+ // snapshot taken from pCtx->sRefPic before this call and later published
+ // to the successor thread context. Left untouched here, sTmpRefPic would
+ // keep pointers to pictures that WelsResetRefPic() just unreferenced
+ // (and that may already be recycled by PrefetchPic()), so the successor
+ // frame would inherit a stale/dangling reference list. Re-sync it to the
+ // freshly-cleared sRefPic; the entries were already unreffed once by
+ // WelsResetRefPic() above, so do not call SetUnRef again here.
+ *pRefPic = pCtx->sRefPic;
+ }
pCtx->pLastDecPicInfo->bLastHasMmco5 = true;
break;
case MMCO_LONG:
diff --git a/test/decoder/DecUT_ManageRef.cpp b/test/decoder/DecUT_ManageRef.cpp
new file mode 100644
index 00000000..5f3e1068
--- /dev/null
+++ b/test/decoder/DecUT_ManageRef.cpp
@@ -0,0 +1,117 @@
+#include <gtest/gtest.h>
+#include <cstring>
+
+#include "decoder_context.h"
+#include "manage_dec_ref.h"
+
+using namespace WelsDec;
+
+// Regression test for the MMCO5 stale sTmpRefPic snapshot.
+//
+// In the threaded predecessor-handoff path (decoder_core.cpp), the decoder
+// snapshots pCtx->sRefPic into pCtx->sTmpRefPic, calls WelsMarkAsRef() with a
+// non-NULL pLastDec (which makes WelsMarkAsRef() operate on sTmpRefPic rather
+// than sRefPic), and later publishes the (possibly mutated) sTmpRefPic back
+// into the successor context's sRefPic. Before the fix, MMCO_RESET (MMCO5)
+// called WelsResetRefPic(pCtx), which hard-codes &pCtx->sRefPic and ignores
+// the active list the caller selected, so sTmpRefPic kept a pointer to a
+// picture that had just been unreferenced and made recyclable.
+//
+// This test drives WelsMarkAsRef() directly (bypassing the full decode
+// pipeline) with a synthetic MMCO5 marking and a pre-populated stale
+// short-term reference shared between sRefPic and sTmpRefPic (mirroring the
+// real "sTmpRefPic = sRefPic" snapshot), then asserts the stale entry does
+// not survive in sTmpRefPic. Unlike an end-to-end bitstream decode, this is
+// deterministic and isolated from unrelated threaded-decode defects that can
+// otherwise also crash a full decode of a real stream under threading.
+class ManageDecRefMmco5Test : public ::testing::Test {
+ protected:
+ virtual void SetUp() {
+ memset (&ctx_, 0, sizeof (ctx_));
+ memset (&dqLayer_, 0, sizeof (dqLayer_));
+ memset (&refMarking_, 0, sizeof (refMarking_));
+ memset (&sps_, 0, sizeof (sps_));
+ memset (&pps_, 0, sizeof (pps_));
+ memset (&lastDecPicInfo_, 0, sizeof (lastDecPicInfo_));
+ memset (&nalUnit_, 0, sizeof (nalUnit_));
+ memset (&stalePic_, 0, sizeof (stalePic_));
+ memset (&newPic_, 0, sizeof (newPic_));
+
+ // A non-IDR slice signalling adaptive reference picture marking with a
+ // single MMCO_RESET (MMCO5) command.
+ refMarking_.bAdaptiveRefPicMarkingModeFlag = true;
+ refMarking_.sMmcoRef[0].uiMmcoType = MMCO_RESET;
+ refMarking_.sMmcoRef[1].uiMmcoType = MMCO_END;
+
+ sps_.iSpsId = 0;
+ sps_.iNumRefFrames = 4;
+ sps_.uiLog2MaxFrameNum = 4;
+ pps_.iPpsId = 0;
+
+ dqLayer_.pRefPicMarking = &refMarking_;
+ dqLayer_.sLayerInfo.pSps = &sps_; // read directly by MMCO()
+
+ nalUnit_.sNalHeaderExt.sNalUnitHeader.eNalUnitType = NAL_UNIT_CODED_SLICE; // non-IDR
+ nalUnit_.sNalHeaderExt.bIdrFlag = false;
+ nalUnitPtr_ = &nalUnit_;
+
+ accessUnit_.pNalUnitsList = &nalUnitPtr_;
+ accessUnit_.uiStartPos = 0;
+ accessUnit_.uiEndPos = 0;
+
+ ctx_.pCurDqLayer = &dqLayer_;
+ ctx_.pSps = &sps_;
+ ctx_.pPps = &pps_;
+ ctx_.pAccessUnitList = &accessUnit_;
+ ctx_.pLastDecPicInfo = &lastDecPicInfo_;
+
+ // Stale short-term reference picture shared by sRefPic and sTmpRefPic,
+ // mirroring the real threaded snapshot (sTmpRefPic = sRefPic is a plain
+ // struct copy of the same picture pointers, done once in decoder_core.cpp
+ // just before WelsMarkAsRef() is called).
+ stalePic_.iRefCount = 0; // eligible for SetUnRef() to clear it
+ stalePic_.eSliceType = I_SLICE; // makes SetUnRef() return before it
+ // touches pRefPic[list][], which this
+ // test does not populate
+ stalePic_.iFrameNum = 999; // must differ from newPic_.iFrameNum so
+ // AddShortTermToList() does not treat
+ // this as a duplicate-frame_num replace
+
+ ctx_.sRefPic.pShortRefList[LIST_0][0] = &stalePic_;
+ ctx_.sRefPic.uiShortRefCount[LIST_0] = 1;
+ ctx_.sTmpRefPic = ctx_.sRefPic; // the real snapshot idiom
+
+ newPic_.iFrameNum = 0;
+ }
+
+ SWelsDecoderContext ctx_;
+ SDqLayer dqLayer_;
+ SRefPicMarking refMarking_;
+ SSps sps_;
+ SPps pps_;
+ SWelsLastDecPicInfo lastDecPicInfo_;
+ SNalUnit nalUnit_;
+ PNalUnit nalUnitPtr_;
+ SAccessUnit accessUnit_;
+ SPicture stalePic_;
+ SPicture newPic_;
+};
+
+TEST_F (ManageDecRefMmco5Test, Mmco5ResetInvalidatesThreadedSnapshot) {
+ ASSERT_EQ (0, WelsMarkAsRef (&ctx_, &newPic_));
+
+ // With the bug, the pre-reset stale pointer survives in sTmpRefPic
+ // (shifted, not cleared) alongside the newly-added picture, inflating the
+ // short-term ref count to 2. With the fix, only the newly-added picture
+ // remains.
+ EXPECT_EQ (1u, ctx_.sTmpRefPic.uiShortRefCount[LIST_0]);
+ for (uint32_t i = 0; i < ctx_.sTmpRefPic.uiShortRefCount[LIST_0]; ++i) {
+ EXPECT_NE (&stalePic_, ctx_.sTmpRefPic.pShortRefList[LIST_0][i])
+ << "stale reference picture pointer leaked into sTmpRefPic after MMCO5 reset";
+ }
+
+ // sRefPic (the persistent, non-threaded list) is always cleared directly
+ // by WelsResetRefPic(), independent of this fix.
+ EXPECT_EQ (0u, ctx_.sRefPic.uiShortRefCount[LIST_0]);
+ EXPECT_EQ (0u, ctx_.sRefPic.uiLongRefCount[LIST_0]);
+}
diff --git a/test/decoder/targets.mk b/test/decoder/targets.mk
index 47df6777..a4476c56 100644
--- a/test/decoder/targets.mk
+++ b/test/decoder/targets.mk
@@ -9,6 +9,7 @@ DECODER_UNITTEST_CPP_SRCS=\
$(DECODER_UNITTEST_SRCDIR)/DecUT_ErrorConcealment.cpp\
$(DECODER_UNITTEST_SRCDIR)/DecUT_IdctResAddPred.cpp\
$(DECODER_UNITTEST_SRCDIR)/DecUT_IntraPrediction.cpp\
+ $(DECODER_UNITTEST_SRCDIR)/DecUT_ManageRef.cpp\
$(DECODER_UNITTEST_SRCDIR)/DecUT_ParseSyntax.cpp\
$(DECODER_UNITTEST_SRCDIR)/DecUT_PredMv.cpp\