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\