Commit 1a0073f0 for openh264

commit 1a0073f0322c8b74cbcb75ca1bb1c3d19d75538d
Author: tyan0 <takashi.yano@nifty.ne.jp>
Date:   Wed Sep 23 17:27:21 2026 +0900

    Re-land #4002 and #4010 with a fix for the stall they caused (#4019)

    * Reapply "Revert PR4002 && PR4010 (#4015)"

    #4002 and #4010 went out together in #4015: #4002 made threaded decoding stall
    after ten to fifteen seconds (#4014), and #4010 sits on top of the barrier
    #4002 introduced, so it could not stay behind on its own.

    That stall has a cause and a fix, which is the commit after this one.  #4002
    was guarding real problems -- concurrent DPB reallocation, and allocator
    accounting across contexts -- and #4010 fixes a race that only exists once
    #4002 is in, so leaving both out trades one defect for two.

    This restores exactly what #4015 removed, including the threaded regression
    tests that went with it.  The fix is a separate commit so the two can be
    reviewed apart, and so this one can be reverted again on its own if the
    verification against the original reproduction does not come out clean.

    * decoder: release references dropped by a reference-list copy

    Threaded decoding moves reference-picture state between worker contexts by
    assigning the whole SRefPic in DecodeCurrentAccessUnit():

        pLastThreadCtx->pCtx->sTmpRefPic = pLastThreadCtx->pCtx->sRefPic;
        WelsMarkAsRef (pLastThreadCtx->pCtx, pLastThreadCtx->pDec);
        pCtx->sRefPic = pLastThreadCtx->pCtx->sTmpRefPic;

    The destination lists are replaced wholesale.  A picture the destination was
    holding that the source does not hold leaves every reference list at that
    instant with bUsedAsRef still set, and because SetUnRef() was never called on
    it there is no deferred unref armed either, so nothing can release it again.
    Its buffer never returns to the pool.

    Once the pool is empty PrefetchPic() returns NULL and the decode stops with

        Error:DecodeCurrentAccessUnit()::::::PrefetchPic ERROR, pSps->iNumRefFrames:6.

    This is what #4014 saw as video stuck on one frame after ten to fifteen
    seconds while audio continued.  The assignment is older than #4002; what #4002
    changed is that the two contexts' lists began to diverge in a way that makes it
    bite.

    Measured on a 1280x720 High profile stream (pic_order_cnt_type 2,
    log2_max_frame_num_minus4 0 so MaxFrameNum is 16, num_ref_frames 6, P slices
    only) through a harness that calls DecodeFrameNoDelay() and takes each frame
    immediately, so no player is involved.  Accounting for every buffer
    PrefetchPic() will not hand out, sampled before each call:

                    capacity  used-as-ref      held  pinned
        threads=1          8  6, steady           1       0
        threads=2         19  climbs to 16      1-2     0-7

    num_ref_frames is 6, so 16 is the whole DPB saturating; the output path and the
    pin count were steady.  Instrumenting the copy to report what it drops gave 11
    losses, all at this assignment, against 11 pictures left marked and unlisted at
    the end -- the counts match, and each one costs the pool a buffer for good.

    Release what the copy is about to drop.  With this, the same stream decodes to
    completion at one, two and three threads over a 480-frame and a 1440-frame
    excerpt, with no PrefetchPic error and no stall, and the three thread counts
    produce identical digests.

    * decoder: do not touch reference-list entries the buffer no longer owns

    WelsReleaseDroppedRefs() reads bUsedAsRef off every picture the destination
    list holds.  One of the three assignments it guards is reached exactly when the
    DPB has been freed or reallocated -- the comment above that branch says so --
    and the lists of a context overtaken by that can name pictures which no longer
    exist.  Reading one of those is a use-after-free.

    It survives on x86, where the freed allocation is usually still mapped, which
    is why it got through local testing here and through the verification on macOS,
    Linux and Windows.  Under qemu-aarch64 it faults, which is where CI caught it:

        ThreadDecoderPreviousPicRaceTest.ThreadedOutputIsConsistent ... signal 11

    Check each candidate against the live picture buffer before touching it.  A
    reallocation that preserves the picture objects still releases them, because
    they are still in the buffer; one that freed them skips them, because they are
    not.  The list walks are also clamped to the array bounds rather than trusting
    a count that came from a stale context.

    ---------

    Co-authored-by: tyan0 <takashi.yano.rh@hitachi.com>

diff --git a/codec/decoder/core/inc/manage_dec_ref.h b/codec/decoder/core/inc/manage_dec_ref.h
index 164ae15d..5208a3ab 100644
--- a/codec/decoder/core/inc/manage_dec_ref.h
+++ b/codec/decoder/core/inc/manage_dec_ref.h
@@ -49,6 +49,7 @@ namespace WelsDec {

 void  WelsResetRefPic (PWelsDecoderContext pCtx);
 void  WelsResetRefPicWithoutUnRef (PWelsDecoderContext pCtx);
+void  WelsReleaseDroppedRefs (PPicBuff pPicBuf, PRefPic pDst, PRefPic pSrc);
 int32_t WelsInitRefList (PWelsDecoderContext pCtx, int32_t iPoc);
 int32_t WelsInitBSliceRefList (PWelsDecoderContext pCtx, int32_t iPoc);
 int32_t WelsReorderRefList (PWelsDecoderContext pCtx);
diff --git a/codec/decoder/core/inc/pic_queue.h b/codec/decoder/core/inc/pic_queue.h
index 473f80ff..b41bb916 100644
--- a/codec/decoder/core/inc/pic_queue.h
+++ b/codec/decoder/core/inc/pic_queue.h
@@ -37,6 +37,10 @@

 #include "picture.h"

+namespace WelsCommon {
+class CMemoryAlign;
+}
+
 namespace WelsDec {

 #define   PICTURE_RESOLUTION_ALIGNMENT      32
@@ -46,6 +50,7 @@ typedef struct TagPicBuff {
   PPicture*      ppPic;
   int32_t        iCapacity;  // capacity size of queue
   int32_t        iCurrentIdx;
+  WelsCommon::CMemoryAlign* pMa;  // allocator that owns this buffer's memory; used to free it
 } SPicBuff, *PPicBuff;

 /*
diff --git a/codec/decoder/core/src/decoder.cpp b/codec/decoder/core/src/decoder.cpp
index 198b3e5a..9d05a9b0 100644
--- a/codec/decoder/core/src/decoder.cpp
+++ b/codec/decoder/core/src/decoder.cpp
@@ -76,6 +76,7 @@ static int32_t CreatePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, cons
   if (NULL == pPicBuf) {
     return ERR_INFO_OUT_OF_MEMORY;
   }
+  pPicBuf->pMa = pMa;

   pPicBuf->ppPic = (PPicture*)pMa->WelsMallocz (kiSize * sizeof (PPicture), "PPicture*");

@@ -114,11 +115,13 @@ static int32_t IncreasePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, co
   }

   CMemoryAlign* pMa = pCtx->pMemAlign;
+  CMemoryAlign* pOldMa = (pPicOldBuf != NULL && pPicOldBuf->pMa != NULL) ? pPicOldBuf->pMa : pMa;
   pPicNewBuf = (PPicBuff)pMa->WelsMallocz (sizeof (SPicBuff), "PPicBuff");

   if (NULL == pPicNewBuf) {
     return ERR_INFO_OUT_OF_MEMORY;
   }
+  pPicNewBuf->pMa = pMa;

   pPicNewBuf->ppPic = (PPicture*)pMa->WelsMallocz (kiNewSize * sizeof (PPicture), "PPicture*");

@@ -158,12 +161,12 @@ static int32_t IncreasePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, co
   }
 // remove old PicBuf
   if (pPicOldBuf->ppPic != NULL) {
-    pMa->WelsFree (pPicOldBuf->ppPic, "pPicOldBuf->queue");
+    pOldMa->WelsFree (pPicOldBuf->ppPic, "pPicOldBuf->queue");
     pPicOldBuf->ppPic = NULL;
   }
   pPicOldBuf->iCapacity = 0;
   pPicOldBuf->iCurrentIdx = 0;
-  pMa->WelsFree (pPicOldBuf, "pPicOldBuf");
+  pOldMa->WelsFree (pPicOldBuf, "pPicOldBuf");
   pPicOldBuf = NULL;
   return ERR_NONE;
 }
@@ -178,12 +181,14 @@ static int32_t DecreasePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, co
   }

   CMemoryAlign* pMa = pCtx->pMemAlign;
+  CMemoryAlign* pOldMa = (pPicOldBuf != NULL && pPicOldBuf->pMa != NULL) ? pPicOldBuf->pMa : pMa;

   pPicNewBuf = (PPicBuff)pMa->WelsMallocz (sizeof (SPicBuff), "PPicBuff");

   if (NULL == pPicNewBuf) {
     return ERR_INFO_OUT_OF_MEMORY;
   }
+  pPicNewBuf->pMa = pMa;

   pPicNewBuf->ppPic = (PPicture*)pMa->WelsMallocz (kiNewSize * sizeof (PPicture), "PPicture*");

@@ -228,7 +233,7 @@ static int32_t DecreasePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, co
   for (iPicIdx = iDelIdx; iPicIdx < kiOldSize; iPicIdx++) {
     if (iPrevPicIdx != iPicIdx) {
       if (pPicOldBuf->ppPic[iPicIdx] != NULL) {
-        FreePicture (pPicOldBuf->ppPic[iPicIdx], pMa);
+        FreePicture (pPicOldBuf->ppPic[iPicIdx], pOldMa);
         pPicOldBuf->ppPic[iPicIdx] = NULL;
       }
     }
@@ -247,12 +252,12 @@ static int32_t DecreasePicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, co
   }
   // remove old PicBuf
   if (pPicOldBuf->ppPic != NULL) {
-    pMa->WelsFree (pPicOldBuf->ppPic, "pPicOldBuf->queue");
+    pOldMa->WelsFree (pPicOldBuf->ppPic, "pPicOldBuf->queue");
     pPicOldBuf->ppPic = NULL;
   }
   pPicOldBuf->iCapacity = 0;
   pPicOldBuf->iCurrentIdx = 0;
-  pMa->WelsFree (pPicOldBuf, "pPicOldBuf");
+  pOldMa->WelsFree (pPicOldBuf, "pPicOldBuf");
   pPicOldBuf = NULL;

   return ERR_NONE;
@@ -267,6 +272,9 @@ void DestroyPicBuff (PWelsDecoderContext pCtx, PPicBuff* ppPicBuf, CMemoryAlign*
     return;

   pPicBuf = *ppPicBuf;
+  // Free through the allocator that created this buffer.
+  if (pPicBuf->pMa != NULL)
+    pMa = pPicBuf->pMa;
   while (pPicBuf->ppPic != NULL) {
     int32_t iPicIdx = 0;
     while (iPicIdx < pPicBuf->iCapacity) {
diff --git a/codec/decoder/core/src/decoder_core.cpp b/codec/decoder/core/src/decoder_core.cpp
index c0990a07..39e28104 100644
--- a/codec/decoder/core/src/decoder_core.cpp
+++ b/codec/decoder/core/src/decoder_core.cpp
@@ -2591,14 +2591,20 @@ int32_t DecodeCurrentAccessUnit (PWelsDecoderContext pCtx, uint8_t** ppDst, SBuf
             }
             pLastThreadCtx->pCtx->sTmpRefPic = pLastThreadCtx->pCtx->sRefPic;
             WelsMarkAsRef (pLastThreadCtx->pCtx, pLastThreadCtx->pDec);
+            //The assignment below replaces these lists wholesale; release what it drops.
+            WelsReleaseDroppedRefs (pCtx->pPicBuff, &pCtx->sRefPic, &pLastThreadCtx->pCtx->sTmpRefPic);
             pCtx->sRefPic = pLastThreadCtx->pCtx->sTmpRefPic;
           } else {
+            //The assignment below replaces these lists wholesale; release what it drops.
+            WelsReleaseDroppedRefs (pCtx->pPicBuff, &pCtx->sRefPic, &pLastThreadCtx->pCtx->sRefPic);
             pCtx->sRefPic = pLastThreadCtx->pCtx->sRefPic;
           }
         } else {
           // The cached last-thread picture no longer maps to a live buffer
           // entry (DPB was freed/reallocated).  Copy reference state without
           // marking a stale picture as reference.
+          //The assignment below replaces these lists wholesale; release what it drops.
+          WelsReleaseDroppedRefs (pCtx->pPicBuff, &pCtx->sRefPic, &pLastThreadCtx->pCtx->sRefPic);
           pCtx->sRefPic = pLastThreadCtx->pCtx->sRefPic;
         }
       }
diff --git a/codec/decoder/core/src/manage_dec_ref.cpp b/codec/decoder/core/src/manage_dec_ref.cpp
index c164272f..d884c08c 100644
--- a/codec/decoder/core/src/manage_dec_ref.cpp
+++ b/codec/decoder/core/src/manage_dec_ref.cpp
@@ -128,6 +128,52 @@ void WelsResetRefPic (PWelsDecoderContext pCtx) {
   pRefPic->uiLongRefCount[LIST_0] = 0;
 }

+//Release what a whole-struct copy of SRefPic is about to drop. Every picture the destination
+//still lists, and the source does not, leaves the reference lists when the copy lands; without
+//this it keeps bUsedAsRef set with no list entry and no armed unref, and its buffer never
+//returns to the pool.
+//Is this pointer still one of the pictures the buffer owns? A context that has been overtaken
+//by a DPB reallocation can list pictures that no longer exist, and those must not be touched.
+static bool IsLivePicture (PPicBuff pPicBuf, PPicture pPic) {
+  if (pPicBuf == NULL || pPic == NULL)
+    return false;
+  for (int32_t i = 0; i < pPicBuf->iCapacity; ++i) {
+    if (pPicBuf->ppPic[i] == pPic)
+      return true;
+  }
+  return false;
+}
+
+void WelsReleaseDroppedRefs (PPicBuff pPicBuf, PRefPic pDst, PRefPic pSrc) {
+  if (pPicBuf == NULL || pDst == NULL || pSrc == NULL || pDst == pSrc)
+    return;
+  const int32_t kiShort = WELS_MIN ((int32_t) pDst->uiShortRefCount[LIST_0], MAX_DPB_COUNT);
+  const int32_t kiLong  = WELS_MIN ((int32_t) pDst->uiLongRefCount[LIST_0], MAX_DPB_COUNT);
+  PPicture pHeld[MAX_DPB_COUNT * 2];
+  int32_t iHeld = 0;
+  for (int32_t i = 0; i < kiShort; ++i)
+    if (IsLivePicture (pPicBuf, pDst->pShortRefList[LIST_0][i]))
+      pHeld[iHeld++] = pDst->pShortRefList[LIST_0][i];
+  for (int32_t i = 0; i < kiLong; ++i)
+    if (IsLivePicture (pPicBuf, pDst->pLongRefList[LIST_0][i]))
+      pHeld[iHeld++] = pDst->pLongRefList[LIST_0][i];
+
+  for (int32_t i = 0; i < iHeld; ++i) {
+    PPicture pPic = pHeld[i];
+    if (!pPic->bUsedAsRef)
+      continue;
+    bool bKept = false;
+    const int32_t kiSrcShort = WELS_MIN ((int32_t) pSrc->uiShortRefCount[LIST_0], MAX_DPB_COUNT);
+    const int32_t kiSrcLong  = WELS_MIN ((int32_t) pSrc->uiLongRefCount[LIST_0], MAX_DPB_COUNT);
+    for (int32_t j = 0; !bKept && j < kiSrcShort; ++j)
+      bKept = (pSrc->pShortRefList[LIST_0][j] == pPic);
+    for (int32_t j = 0; !bKept && j < kiSrcLong; ++j)
+      bKept = (pSrc->pLongRefList[LIST_0][j] == pPic);
+    if (!bKept)
+      SetUnRef (pPic);
+  }
+}
+
 void WelsResetRefPicWithoutUnRef (PWelsDecoderContext pCtx) {
   int32_t i = 0;
   PRefPic pRefPic = &pCtx->sRefPic;
diff --git a/codec/decoder/plus/src/welsDecoderExt.cpp b/codec/decoder/plus/src/welsDecoderExt.cpp
index 73ead68d..8f760eb8 100644
--- a/codec/decoder/plus/src/welsDecoderExt.cpp
+++ b/codec/decoder/plus/src/welsDecoderExt.cpp
@@ -1373,12 +1373,51 @@ DECODING_STATE CWelsDecoder::ParseAccessUnit (SWelsDecoderThreadCTX& sThreadCtx)
   int32_t iRet = DecodeFrame2WithCtx (sThreadCtx.pCtx, sThreadCtx.kpSrc, sThreadCtx.kiSrcLen, sThreadCtx.ppDst,
                                       &sThreadCtx.sDstInfo);

-  int32_t iErr = InitConstructAccessUnit (sThreadCtx.pCtx, &sThreadCtx.sDstInfo);
+  int32_t iErr = WelsDecodeInitAccessUnitStart (sThreadCtx.pCtx, &sThreadCtx.sDstInfo);
   if (ERR_NONE != iErr) {
     return (DECODING_STATE) (iRet | iErr);
   }
   if (sThreadCtx.pCtx->bNewSeqBegin) {
+    if (GetThreadCount (sThreadCtx.pCtx) > 1) {
+      // Wait for older workers before replacing shared DPB storage.
+      for (int32_t i = 0; i < m_DecCtxActiveCount; ++i) {
+        if (m_pDecThrCtxActive[i] != NULL && m_pDecThrCtxActive[i] != &sThreadCtx) {
+          WAIT_SEMAPHORE (&m_pDecThrCtxActive[i]->sThreadInfo.sIsIdle, WELS_DEC_THREAD_WAIT_INFINITE);
+          RELEASE_SEMAPHORE (&m_pDecThrCtxActive[i]->sThreadInfo.sIsIdle);
+        }
+      }
+      for (int32_t i = 0; i < m_iCtxCount; ++i) {
+        //Past the barrier nothing is decoding and the chain is cut for every context, so a
+        //hand-off signal still raised has lost the consumer that would have reset it. Left
+        //raised, the next frame that waits on that worker passes the wait at once and marks
+        //a picture that has not been decoded.
+        if (m_pDecThrCtx[i].pCtx != NULL)
+          RESET_EVENT (&m_pDecThrCtx[i].sSliceDecodeStart);
+      }
+      sThreadCtx.pCtx->pLastThreadCtx = NULL;
+    }
+    iErr = AllocPicBuffOnNewSeqBegin (sThreadCtx.pCtx);
+    if (ERR_NONE != iErr) {
+      return (DECODING_STATE) (iRet | iErr);
+    }
     m_pPicBuff = sThreadCtx.pCtx->pPicBuff;
+    // Keep sibling contexts from carrying stale DPB references.
+    for (int32_t i = 0; i < m_iCtxCount; ++i) {
+      if (&m_pDecThrCtx[i] != &sThreadCtx && m_pDecThrCtx[i].pCtx != NULL) {
+        m_pDecThrCtx[i].pCtx->pPicBuff = m_pPicBuff;
+        m_pDecThrCtx[i].pCtx->bHaveGotMemory = sThreadCtx.pCtx->bHaveGotMemory;
+        m_pDecThrCtx[i].pCtx->iPicQueueNumber = sThreadCtx.pCtx->iPicQueueNumber;
+        m_pDecThrCtx[i].pCtx->iImgWidthInPixel = sThreadCtx.pCtx->iImgWidthInPixel;
+        m_pDecThrCtx[i].pCtx->iImgHeightInPixel = sThreadCtx.pCtx->iImgHeightInPixel;
+        m_pDecThrCtx[i].pCtx->pDec = NULL;
+        m_pDecThrCtx[i].pCtx->pLastThreadCtx = NULL;
+        iErr = InitialDqLayersContext (m_pDecThrCtx[i].pCtx, sThreadCtx.pCtx->iImgWidthInPixel,
+                                       sThreadCtx.pCtx->iImgHeightInPixel);
+        if (ERR_NONE != iErr) {
+          return (DECODING_STATE) (iRet | iErr);
+        }
+      }
+    }
   } else if (bPicBuffChanged) {
     InitialDqLayersContext (sThreadCtx.pCtx, sThreadCtx.pCtx->pSps->iMbWidth << 4, sThreadCtx.pCtx->pSps->iMbHeight << 4);
   }
diff --git a/test/BaseThreadDecoderTest.h b/test/BaseThreadDecoderTest.h
index f9c317ea..7263b323 100644
--- a/test/BaseThreadDecoderTest.h
+++ b/test/BaseThreadDecoderTest.h
@@ -38,6 +38,8 @@ class BaseThreadDecoderTest {
   int32_t SetUp();
   void TearDown();
   bool ThreadDecodeFile (const char* fileName, Callback* cbk);
+  // Decode alternating IDR frames from two streams to force repeated sequence changes.
+  bool ThreadDecodeResolutionSwitch (const char* fileName1, const char* fileName2, int32_t iterations, Callback* cbk);

   bool Open (const char* fileName);
   ISVCDecoder* decoder_;
diff --git a/test/api/BaseThreadDecoderTest.cpp b/test/api/BaseThreadDecoderTest.cpp
index 52403f7d..487de5c8 100644
--- a/test/api/BaseThreadDecoderTest.cpp
+++ b/test/api/BaseThreadDecoderTest.cpp
@@ -307,6 +307,58 @@ bool BaseThreadDecoderTest::ThreadDecodeFile (const char* fileName, Callback* cb
   return true;
 }

+static bool ReadFileToBuffer (const char* fileName, BufferedData& buf) {
+  std::ifstream file (fileName, std::ios::in | std::ios::binary);
+  if (!file.is_open())
+    return false;
+  char b;
+  for (;;) {
+    file.read (&b, 1);
+    if (file.gcount() != 1) // end of file
+      break;
+    if (!buf.PushBack (b))
+      return false;
+  }
+  return true;
+}
+
+bool BaseThreadDecoderTest::ThreadDecodeResolutionSwitch (const char* fileName1, const char* fileName2,
+    int32_t iterations, Callback* cbk) {
+  BufferedData buf1;
+  BufferedData buf2;
+  if (!ReadFileToBuffer (fileName1, buf1) || !ReadFileToBuffer (fileName2, buf2))
+    return false;
+
+  int32_t len1 = (int32_t) buf1.Length();
+  int32_t len2 = (int32_t) buf2.Length();
+  int32_t pos1 = 0;
+  int32_t pos2 = 0;
+  int32_t frameSize1 = ReadFrame (buf1.data(), len1, pos1);
+  int32_t frameSize2 = ReadFrame (buf2.data(), len2, pos2);
+  if (frameSize1 <= 0 || frameSize2 <= 0)
+    return false;
+
+  uiTimeStamp = 0;
+  memset (&sBufInfo, 0, sizeof (SBufferInfo));
+  for (int32_t i = 0; i < iterations; ++i) {
+    DecodeFrame (buf1.data(), frameSize1, cbk);
+    if (::testing::Test::HasFatalFailure())
+      return false;
+    DecodeFrame (buf2.data(), frameSize2, cbk);
+    if (::testing::Test::HasFatalFailure())
+      return false;
+  }
+
+  int32_t iEndOfStreamFlag = 1;
+  decoder_->SetOption (DECODER_OPTION_END_OF_STREAM, &iEndOfStreamFlag);
+  int32_t num_of_frames_in_buffer = 0;
+  decoder_->GetOption (DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, &num_of_frames_in_buffer);
+  for (int32_t i = 0; i < num_of_frames_in_buffer; ++i) {
+    FlushFrame (cbk);
+  }
+  return true;
+}
+
 bool BaseThreadDecoderTest::Open (const char* fileName) {
   if (decodeStatus_ == OpenFile) {
     file_.open (fileName, std::ios_base::out | std::ios_base::binary);
diff --git a/test/api/thread_decoder_test.cpp b/test/api/thread_decoder_test.cpp
index 852acb42..3e181b96 100644
--- a/test/api/thread_decoder_test.cpp
+++ b/test/api/thread_decoder_test.cpp
@@ -119,6 +119,329 @@ static int32_t ReadFrameForHangRegression (uint8_t* pBuf, const int32_t& iFileSi
   return bytesAvailable;
 }

+struct SpsInfoForRefQueueSwitch {
+  int32_t iWidth;
+  int32_t iHeight;
+  int32_t iNumRefFrames;
+};
+
+class CBitReaderForRefQueueSwitch {
+ public:
+  CBitReaderForRefQueueSwitch (const uint8_t* pData, const size_t iDataSize)
+    : pData_ (pData), iBitLength_ (iDataSize * 8), iBitPos_ (0) {
+  }
+
+  bool ReadBit (uint32_t* pBit) {
+    if (pBit == NULL || iBitPos_ >= iBitLength_) {
+      return false;
+    }
+
+    const size_t iByteOffset = iBitPos_ >> 3;
+    const int32_t iBitOffset = 7 - static_cast<int32_t> (iBitPos_ & 7);
+    *pBit = (pData_[iByteOffset] >> iBitOffset) & 1;
+    ++iBitPos_;
+    return true;
+  }
+
+  bool ReadBits (const int32_t iNumBits, uint32_t* pValue) {
+    if (pValue == NULL || iNumBits <= 0 || iNumBits > 32) {
+      return false;
+    }
+
+    uint32_t uiValue = 0;
+    for (int32_t i = 0; i < iNumBits; ++i) {
+      uint32_t uiBit = 0;
+      if (!ReadBit (&uiBit)) {
+        return false;
+      }
+      uiValue = (uiValue << 1) | uiBit;
+    }
+
+    *pValue = uiValue;
+    return true;
+  }
+
+  bool ReadUe (uint32_t* pValue) {
+    if (pValue == NULL) {
+      return false;
+    }
+
+    int32_t iLeadingZeros = 0;
+    uint32_t uiBit = 0;
+    while (true) {
+      if (!ReadBit (&uiBit)) {
+        return false;
+      }
+      if (uiBit == 1) {
+        break;
+      }
+      ++iLeadingZeros;
+      if (iLeadingZeros > 31) {
+        return false;
+      }
+    }
+
+    if (iLeadingZeros == 0) {
+      *pValue = 0;
+      return true;
+    }
+
+    uint32_t uiInfoBits = 0;
+    if (!ReadBits (iLeadingZeros, &uiInfoBits)) {
+      return false;
+    }
+
+    *pValue = ((1u << iLeadingZeros) - 1u) + uiInfoBits;
+    return true;
+  }
+
+  bool ReadSe (int32_t* pValue) {
+    if (pValue == NULL) {
+      return false;
+    }
+
+    uint32_t uiUeValue = 0;
+    if (!ReadUe (&uiUeValue)) {
+      return false;
+    }
+
+    if (uiUeValue & 1) {
+      *pValue = static_cast<int32_t> ((uiUeValue + 1) >> 1);
+    } else {
+      *pValue = -static_cast<int32_t> (uiUeValue >> 1);
+    }
+    return true;
+  }
+
+ private:
+  const uint8_t* pData_;
+  size_t iBitLength_;
+  size_t iBitPos_;
+};
+
+static bool SkipScalingListForRefQueueSwitch (CBitReaderForRefQueueSwitch& sBitReader, const int32_t iListSize) {
+  int32_t iLastScale = 8;
+  int32_t iNextScale = 8;
+
+  for (int32_t i = 0; i < iListSize; ++i) {
+    if (iNextScale != 0) {
+      int32_t iDeltaScale = 0;
+      if (!sBitReader.ReadSe (&iDeltaScale)) {
+        return false;
+      }
+      iNextScale = (iLastScale + iDeltaScale + 256) % 256;
+    }
+    iLastScale = (iNextScale == 0) ? iLastScale : iNextScale;
+  }
+  return true;
+}
+
+static size_t GetStartCodeLengthForRefQueueSwitch (const uint8_t* pData, const size_t iDataSize) {
+  if (iDataSize >= 4 && pData[0] == 0 && pData[1] == 0 && pData[2] == 0 && pData[3] == 1) {
+    return 4;
+  }
+  if (iDataSize >= 3 && pData[0] == 0 && pData[1] == 0 && pData[2] == 1) {
+    return 3;
+  }
+  return 0;
+}
+
+static bool ExtractFirstSpsRbspForRefQueueSwitch (const std::vector<uint8_t>& bitstream, std::vector<uint8_t>* pRbsp) {
+  if (pRbsp == NULL || bitstream.empty()) {
+    return false;
+  }
+
+  const size_t iBitstreamSize = bitstream.size();
+  size_t i = 0;
+  while (i + 3 < iBitstreamSize) {
+    const size_t iStartCodeLength = GetStartCodeLengthForRefQueueSwitch (&bitstream[i], iBitstreamSize - i);
+    if (iStartCodeLength == 0) {
+      ++i;
+      continue;
+    }
+
+    const size_t iNalStart = i + iStartCodeLength;
+    size_t iNalEnd = iBitstreamSize;
+    for (size_t j = iNalStart; j + 3 < iBitstreamSize; ++j) {
+      if (GetStartCodeLengthForRefQueueSwitch (&bitstream[j], iBitstreamSize - j) != 0) {
+        iNalEnd = j;
+        break;
+      }
+    }
+
+    if (iNalStart < iNalEnd && (bitstream[iNalStart] & 0x1F) == 7) {
+      pRbsp->clear();
+      for (size_t k = iNalStart + 1; k < iNalEnd; ++k) {
+        if (k + 2 < iNalEnd && bitstream[k] == 0 && bitstream[k + 1] == 0 && bitstream[k + 2] == 3) {
+          pRbsp->push_back (0);
+          pRbsp->push_back (0);
+          k += 2;
+          continue;
+        }
+        pRbsp->push_back (bitstream[k]);
+      }
+      return !pRbsp->empty();
+    }
+
+    i = iNalEnd;
+  }
+
+  return false;
+}
+
+static bool ParseSpsForRefQueueSwitch (const std::vector<uint8_t>& rbsp, SpsInfoForRefQueueSwitch* pSpsInfo) {
+  if (pSpsInfo == NULL || rbsp.empty()) {
+    return false;
+  }
+
+  CBitReaderForRefQueueSwitch sBitReader (rbsp.data(), rbsp.size());
+
+  uint32_t uiProfileIdc = 0;
+  uint32_t uiTmp = 0;
+  if (!sBitReader.ReadBits (8, &uiProfileIdc) || !sBitReader.ReadBits (8, &uiTmp)
+      || !sBitReader.ReadBits (8, &uiTmp) || !sBitReader.ReadUe (&uiTmp)) {
+    return false;
+  }
+
+  uint32_t uiChromaFormatIdc = 1;
+  if (uiProfileIdc == 100 || uiProfileIdc == 110 || uiProfileIdc == 122 || uiProfileIdc == 244
+      || uiProfileIdc == 44 || uiProfileIdc == 83 || uiProfileIdc == 86 || uiProfileIdc == 118
+      || uiProfileIdc == 128 || uiProfileIdc == 138 || uiProfileIdc == 139 || uiProfileIdc == 134
+      || uiProfileIdc == 135) {
+    if (!sBitReader.ReadUe (&uiChromaFormatIdc)) {
+      return false;
+    }
+    if (uiChromaFormatIdc == 3 && !sBitReader.ReadBit (&uiTmp)) {
+      return false;
+    }
+    if (!sBitReader.ReadUe (&uiTmp) || !sBitReader.ReadUe (&uiTmp) || !sBitReader.ReadBit (&uiTmp)) {
+      return false;
+    }
+
+    uint32_t uiScalingMatrixPresent = 0;
+    if (!sBitReader.ReadBit (&uiScalingMatrixPresent)) {
+      return false;
+    }
+    if (uiScalingMatrixPresent != 0) {
+      const int32_t iScalingListCount = (uiChromaFormatIdc != 3) ? 8 : 12;
+      for (int32_t i = 0; i < iScalingListCount; ++i) {
+        uint32_t uiScalingListPresent = 0;
+        if (!sBitReader.ReadBit (&uiScalingListPresent)) {
+          return false;
+        }
+        if (uiScalingListPresent != 0 && !SkipScalingListForRefQueueSwitch (sBitReader, i < 6 ? 16 : 64)) {
+          return false;
+        }
+      }
+    }
+  }
+
+  uint32_t uiPicOrderCntType = 0;
+  if (!sBitReader.ReadUe (&uiTmp) || !sBitReader.ReadUe (&uiPicOrderCntType)) {
+    return false;
+  }
+  if (uiPicOrderCntType == 0) {
+    if (!sBitReader.ReadUe (&uiTmp)) {
+      return false;
+    }
+  } else if (uiPicOrderCntType == 1) {
+    int32_t iTmpSe = 0;
+    uint32_t uiCycleCount = 0;
+    if (!sBitReader.ReadBit (&uiTmp) || !sBitReader.ReadSe (&iTmpSe)
+        || !sBitReader.ReadSe (&iTmpSe) || !sBitReader.ReadUe (&uiCycleCount)) {
+      return false;
+    }
+    for (uint32_t i = 0; i < uiCycleCount; ++i) {
+      if (!sBitReader.ReadSe (&iTmpSe)) {
+        return false;
+      }
+    }
+  }
+
+  uint32_t uiNumRefFrames = 0;
+  if (!sBitReader.ReadUe (&uiNumRefFrames) || !sBitReader.ReadBit (&uiTmp)) {
+    return false;
+  }
+
+  uint32_t uiPicWidthInMbsMinus1 = 0;
+  uint32_t uiPicHeightInMapUnitsMinus1 = 0;
+  uint32_t uiFrameMbsOnlyFlag = 0;
+  if (!sBitReader.ReadUe (&uiPicWidthInMbsMinus1) || !sBitReader.ReadUe (&uiPicHeightInMapUnitsMinus1)
+      || !sBitReader.ReadBit (&uiFrameMbsOnlyFlag)) {
+    return false;
+  }
+
+  if (uiFrameMbsOnlyFlag == 0 && !sBitReader.ReadBit (&uiTmp)) {
+    return false;
+  }
+  if (!sBitReader.ReadBit (&uiTmp)) {
+    return false;
+  }
+
+  uint32_t uiFrameCroppingFlag = 0;
+  uint32_t uiCropLeft = 0;
+  uint32_t uiCropRight = 0;
+  uint32_t uiCropTop = 0;
+  uint32_t uiCropBottom = 0;
+  if (!sBitReader.ReadBit (&uiFrameCroppingFlag)) {
+    return false;
+  }
+  if (uiFrameCroppingFlag != 0) {
+    if (!sBitReader.ReadUe (&uiCropLeft) || !sBitReader.ReadUe (&uiCropRight)
+        || !sBitReader.ReadUe (&uiCropTop) || !sBitReader.ReadUe (&uiCropBottom)) {
+      return false;
+    }
+  }
+
+  const int32_t iFrameMbsFactor = 2 - static_cast<int32_t> (uiFrameMbsOnlyFlag);
+  int32_t iCropUnitX = 1;
+  int32_t iCropUnitY = iFrameMbsFactor;
+  if (uiChromaFormatIdc == 1) {
+    iCropUnitX = 2;
+    iCropUnitY = 2 * iFrameMbsFactor;
+  } else if (uiChromaFormatIdc == 2) {
+    iCropUnitX = 2;
+  } else if (uiChromaFormatIdc == 3) {
+    iCropUnitY = iFrameMbsFactor;
+  }
+
+  int32_t iWidth = static_cast<int32_t> ((uiPicWidthInMbsMinus1 + 1) * 16);
+  int32_t iHeight = static_cast<int32_t> (iFrameMbsFactor * (uiPicHeightInMapUnitsMinus1 + 1) * 16);
+  iWidth -= static_cast<int32_t> ((uiCropLeft + uiCropRight) * iCropUnitX);
+  iHeight -= static_cast<int32_t> ((uiCropTop + uiCropBottom) * iCropUnitY);
+  if (iWidth <= 0 || iHeight <= 0) {
+    return false;
+  }
+
+  pSpsInfo->iWidth = iWidth;
+  pSpsInfo->iHeight = iHeight;
+  pSpsInfo->iNumRefFrames = static_cast<int32_t> (uiNumRefFrames);
+  return true;
+}
+
+static bool ReadSpsInfoFromBitstreamForRefQueueSwitch (const char* pFileName, SpsInfoForRefQueueSwitch* pSpsInfo) {
+  if (pFileName == NULL || pSpsInfo == NULL) {
+    return false;
+  }
+
+  std::ifstream file (pFileName, std::ios::in | std::ios::binary);
+  if (!file.is_open()) {
+    return false;
+  }
+
+  std::vector<uint8_t> bitstream ((std::istreambuf_iterator<char> (file)), std::istreambuf_iterator<char> ());
+  if (bitstream.empty()) {
+    return false;
+  }
+
+  std::vector<uint8_t> spsRbsp;
+  if (!ExtractFirstSpsRbspForRefQueueSwitch (bitstream, &spsRbsp)) {
+    return false;
+  }
+
+  return ParseSpsForRefQueueSwitch (spsRbsp, pSpsInfo);
+}
+
 class ThreadDecoderHangRegressionTest : public ::testing::Test {
 };

@@ -220,6 +543,43 @@ class ThreadDecoderInitTest : public ::testing::Test, public BaseThreadDecoderTe
 };

 TEST_F (ThreadDecoderInitTest, JustInit) {}
+
+// Regression test for SPARK-801586: repeated threaded sequence changes must
+// not trigger use-after-free in DPB reallocation paths.
+TEST_F (ThreadDecoderInitTest, ThreadedResolutionSwitchNoUseAfterFree) {
+#if defined(ANDROID_NDK)
+  const std::string kPrefix ("/sdcard/");
+#else
+  const std::string kPrefix ("");
+#endif
+  ASSERT_TRUE (ThreadDecodeResolutionSwitch (
+                 (kPrefix + "res/VID_1920x1080_cabac_temporal_direct.264").c_str(),
+                 (kPrefix + "res/QCIF_2P_I_allIPCM.264").c_str(), 100, NULL));
+}
+
+// Regression guard for the same-resolution DPB queue resize path. The selected
+// streams are both 176x144 but have different SPS num_ref_frames values.
+TEST_F (ThreadDecoderInitTest, ThreadedSameResolutionRefQueueResizeNoUseAfterFree) {
+#if defined(ANDROID_NDK)
+  const std::string kPrefix ("/sdcard/");
+#else
+  const std::string kPrefix ("");
+#endif
+
+  const std::string kLowRefStream = kPrefix + "res/BA1_Sony_D.jsv";
+  const std::string kHighRefStream = kPrefix + "res/BA_MW_D.264";
+
+  SpsInfoForRefQueueSwitch sLowRef = {0, 0, 0};
+  SpsInfoForRefQueueSwitch sHighRef = {0, 0, 0};
+  ASSERT_TRUE (ReadSpsInfoFromBitstreamForRefQueueSwitch (kLowRefStream.c_str(), &sLowRef));
+  ASSERT_TRUE (ReadSpsInfoFromBitstreamForRefQueueSwitch (kHighRefStream.c_str(), &sHighRef));
+  ASSERT_EQ (sLowRef.iWidth, sHighRef.iWidth);
+  ASSERT_EQ (sLowRef.iHeight, sHighRef.iHeight);
+  ASSERT_NE (sLowRef.iNumRefFrames, sHighRef.iNumRefFrames);
+
+  ASSERT_TRUE (ThreadDecodeResolutionSwitch (kLowRefStream.c_str(), kHighRefStream.c_str(), 300, NULL));
+}
+
 struct FileParam {
   const char* fileName;
   const char* hashStr;