Commit 5bfbf01c for openh264

commit 5bfbf01cf85a00a4d02f3eecbbc268a116f940ce
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date:   Fri Sep 4 15:06:20 2026 +0800

    decoder: avoid threaded post-return writes to caller ppDst (#4001)

    DecodeFrameNoDelay() hands the caller-owned ppDst pointer array to a worker thread in threaded decode; the worker can keep writing into it after the API call returns, which is unsafe if the caller's storage is short-lived. Route worker output through thread-owned storage instead.

    Adds a regression test that unmaps the caller's ppDst page immediately after each call, draining pending output and waiting for worker completion before teardown.

    Co-authored-by: benzzhan <benzzhan@cisco.com>

diff --git a/codec/decoder/core/inc/decoder_context.h b/codec/decoder/core/inc/decoder_context.h
index 4d9adebc..f1f83d25 100644
--- a/codec/decoder/core/inc/decoder_context.h
+++ b/codec/decoder/core/inc/decoder_context.h
@@ -537,6 +537,7 @@ typedef struct tagSWelsDecThreadCtx {
   void* threadCtxOwner;
   uint8_t* kpSrc;
   int32_t kiSrcLen;
+  uint8_t* pDst[3];
   uint8_t** ppDst;
   SBufferInfo sDstInfo;
   PPicture pDec;
diff --git a/codec/decoder/plus/src/welsDecoderExt.cpp b/codec/decoder/plus/src/welsDecoderExt.cpp
index 73ead68d..b879651d 100644
--- a/codec/decoder/plus/src/welsDecoderExt.cpp
+++ b/codec/decoder/plus/src/welsDecoderExt.cpp
@@ -321,6 +321,7 @@ void CWelsDecoder::OpenDecoderThreads() {
       m_pDecThrCtx[i].threadCtxOwner = this;
       m_pDecThrCtx[i].kpSrc = NULL;
       m_pDecThrCtx[i].kiSrcLen = 0;
+      m_pDecThrCtx[i].pDst[0] = m_pDecThrCtx[i].pDst[1] = m_pDecThrCtx[i].pDst[2] = NULL;
       m_pDecThrCtx[i].ppDst = NULL;
       m_pDecThrCtx[i].pDec = NULL;
       CREATE_EVENT (&m_pDecThrCtx[i].sImageReady, 1, 0, NULL);
@@ -1425,7 +1426,9 @@ int CWelsDecoder::ThreadDecodeFrameInternal (const unsigned char* kpSrc, const i
   }
   m_pDecThrCtx[signal].kpSrc = const_cast<uint8_t*> (kpSrc);
   m_pDecThrCtx[signal].kiSrcLen = kiSrcLen;
-  m_pDecThrCtx[signal].ppDst = ppDst;
+  // Worker threads must not write into caller-owned ppDst after API returns.
+  m_pDecThrCtx[signal].pDst[0] = m_pDecThrCtx[signal].pDst[1] = m_pDecThrCtx[signal].pDst[2] = NULL;
+  m_pDecThrCtx[signal].ppDst = m_pDecThrCtx[signal].pDst;
   memcpy (&m_pDecThrCtx[signal].sDstInfo, pDstInfo, sizeof (SBufferInfo));

   state = ParseAccessUnit (m_pDecThrCtx[signal]);
diff --git a/test/api/thread_decoder_test.cpp b/test/api/thread_decoder_test.cpp
index 0f148fda..4eddcff5 100644
--- a/test/api/thread_decoder_test.cpp
+++ b/test/api/thread_decoder_test.cpp
@@ -7,6 +7,11 @@
 #include <string>
 #include <vector>

+#if !defined(_WIN32)
+#include <sys/mman.h>
+#include <unistd.h>
+#endif
+
 static void UpdateHashFromPlane (SHA1Context* ctx, const uint8_t* plane,
                                  int width, int height, int stride) {
   for (int i = 0; i < height; i++) {
@@ -471,3 +476,81 @@ TEST_F (ThreadDecoderReorderQueueRaceTest, BufferedPictureQueueDrainsAllFrames)
   ASSERT_FALSE (HasFatalFailure());
   EXPECT_EQ (iDecodedFrames_, 50);
 }
+
+// Regression coverage for the threaded ppDst post-return write.
+// In threaded decode, DecodeFrameNoDelay() hands the caller-owned ppDst
+// pointer array to a worker thread; decoding continues asynchronously and can
+// write into ppDst after the API call has already returned. If the caller's
+// storage is short-lived (e.g. freed or unmapped right after the call), that
+// late write is a use-after-free. The worker now writes into thread-owned
+// storage only, so the caller's page can be unmapped immediately after each
+// call without a crash.
+//
+// Iterations are capped: a separate, already-tracked AU-list counter overflow
+// in ResetCurrentAccessUnit surfaces around iteration 13 on this input
+// regardless of this fix, so this test stays inside that bound to isolate the
+// ppDst behavior under test.
+TEST (ThreadDecoderSecurityTest, DecodeFrameNoDelayNoPostReturnWriteToCallerPpDst) {
+#if defined(_WIN32)
+  GTEST_SKIP() << "mmap/munmap based lifetime stress is POSIX-only";
+#else
+  ISVCDecoder* decoder = NULL;
+  ASSERT_EQ (0, WelsCreateDecoder (&decoder));
+  ASSERT_TRUE (decoder != NULL);
+
+  SDecodingParam decParam;
+  memset (&decParam, 0, sizeof (decParam));
+  decParam.uiTargetDqLayer = UCHAR_MAX;
+  decParam.eEcActiveIdc = ERROR_CON_SLICE_COPY;
+  decParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_DEFAULT;
+  int32_t iThreadCount = 2;
+  decoder->SetOption (DECODER_OPTION_NUM_OF_THREADS, &iThreadCount);
+  ASSERT_EQ (0, decoder->Initialize (&decParam));
+
+  std::ifstream file ("res/BA_MW_D.264", std::ios::in | std::ios::binary);
+  ASSERT_TRUE (file.is_open());
+  std::vector<uint8_t> bitstream ((std::istreambuf_iterator<char> (file)), std::istreambuf_iterator<char>());
+  ASSERT_FALSE (bitstream.empty());
+
+  const size_t pageSize = static_cast<size_t> (sysconf (_SC_PAGESIZE));
+  const size_t chunkSize = 1200;
+  const int32_t kMaxIters = 12;
+
+  for (size_t off = 0, iter = 0; off < bitstream.size() && iter < static_cast<size_t> (kMaxIters);
+       off += chunkSize, ++iter) {
+    void* page = mmap (NULL, pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+    ASSERT_NE (MAP_FAILED, page);
+
+    unsigned char** ppDst = reinterpret_cast<unsigned char**> (page);
+    memset (ppDst, 0, pageSize);
+
+    SBufferInfo dstInfo;
+    memset (&dstInfo, 0, sizeof (dstInfo));
+    int32_t len = static_cast<int32_t> (std::min (chunkSize, bitstream.size() - off));
+    DECODING_STATE rv = decoder->DecodeFrameNoDelay (bitstream.data() + off, len, ppDst, &dstInfo);
+    // The 1200-byte offsets are not NAL/AU aligned, so bitstream-parsing-level
+    // states (dsBitstreamError, dsDataErrorConcealed, ...) are expected with
+    // ERROR_CON_SLICE_COPY; only logic-level failures indicate a real problem.
+    EXPECT_EQ (0, rv & (dsInvalidArgument | dsInitialOptExpected | dsOutOfMemory));
+
+    munmap (page, pageSize);
+    usleep (5000);
+  }
+
+  // Drain pipelined in-flight frames before teardown so Uninitialize() does not
+  // free decoder state while a worker thread is still reconstructing.
+  uint8_t* dst[3] = {NULL, NULL, NULL};
+  SBufferInfo info;
+  int32_t endOfStream = 1;
+  decoder->SetOption (DECODER_OPTION_END_OF_STREAM, &endOfStream);
+  int32_t remaining = 0;
+  decoder->GetOption (DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, &remaining);
+  for (int32_t i = 0; i < remaining; ++i) {
+    memset (&info, 0, sizeof (info));
+    decoder->FlushFrame (dst, &info);
+  }
+
+  decoder->Uninitialize();
+  WelsDestroyDecoder (decoder);
+#endif
+}