Commit e22b2731 for openh264

commit e22b2731a3cc116177612eaa3a3f7628dfc00a42
Author: Erik Språng <sprang@google.com>
Date:   Tue Jul 21 10:36:31 2026 +0200

    Fix asymmetric chroma plane stride handling in encoder preprocessing (#3961)

    * Fix asymmetric chroma plane stride handling in encoder preprocessing

    When preprocessing incoming I420 source frames, WelsMoveMemoryWrapper
    previously read only the U chroma plane stride (iStride[1]) and used it as a
    shared stride for copying both the U and V chroma planes. If an incoming
    frame has distinct, asymmetric strides for the U and V chroma planes (for
    example, when the U plane stride is larger than the V plane stride), copying
    the V plane using the U plane's stride advances the source pointer incorrectly.

    This change ensures correct memory advancement for each chroma plane by:
    - Modifying WelsMoveMemory_c to accept separate destination and source
      strides for the U and V chroma planes (iDstStrideU, iDstStrideV,
      iSrcStrideU, iSrcStrideV).
    - Updating WelsMoveMemoryWrapper to read and pass iStride[1] and iStride[2]
      separately from the input SSourcePicture.
    - Updating internal callers (DownsamplePadding and Padding) to pass their
      respective U and V line sizes cleanly.
    - Adding a unit test (CustomChromaPlaneStrides) to verify that distinct,
      asymmetric chroma plane strides are handled correctly.

    * Add chroma stride validation guards and strengthen unit test verification

    ---------

    Co-authored-by: Erik Språng <sprang@webrtc.org>

diff --git a/codec/encoder/core/inc/wels_preprocess.h b/codec/encoder/core/inc/wels_preprocess.h
index 4d1cd8ef..7d4db94d 100644
--- a/codec/encoder/core/inc/wels_preprocess.h
+++ b/codec/encoder/core/inc/wels_preprocess.h
@@ -127,7 +127,7 @@ class CWelsPreProcess {
   int32_t WelsPreprocessReset (sWelsEncCtx* pEncCtx, int32_t iWidth, int32_t iHeight);
   int32_t AllocSpatialPictures (sWelsEncCtx* pCtx, SWelsSvcCodingParam* pParam);
   void    FreeSpatialPictures (sWelsEncCtx* pCtx);
-  int32_t BuildSpatialPicList (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrcPic);
+  int32_t BuildSpatialPicList (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrcPic, int32_t* pSpatialNum);
   int32_t AnalyzeSpatialPic (sWelsEncCtx* pEncCtx, const int32_t kiDIdx);
   int32_t UpdateSpatialPictures (sWelsEncCtx* pEncCtx, SWelsSvcCodingParam* pParam, const int8_t iCurTid,
                                  const int32_t d_idx);
@@ -157,7 +157,7 @@ class CWelsPreProcess {
   int32_t InitLastSpatialPictures (sWelsEncCtx* pEncCtx);

  private:
-  int32_t SingleLayerPreprocess (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrc, Scaled_Picture* m_sScaledPicture);
+  int32_t SingleLayerPreprocess (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrc, Scaled_Picture* m_sScaledPicture, int32_t* pSpatialNum);

   void  BilateralDenoising (SPicture* pSrc, const int32_t iWidth, const int32_t iHeight);

@@ -175,7 +175,7 @@ class CWelsPreProcess {

   int32_t ColorspaceConvert (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
                              const int32_t kiWidth, const int32_t kiHeight);
-  void WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
+  int32_t WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
                               const int32_t kiWidth, const int32_t kiHeight);

   /*!
diff --git a/codec/encoder/core/src/encoder_ext.cpp b/codec/encoder/core/src/encoder_ext.cpp
index c67a6ad2..228a67dc 100644
--- a/codec/encoder/core/src/encoder_ext.cpp
+++ b/codec/encoder/core/src/encoder_ext.cpp
@@ -3486,10 +3486,10 @@ int32_t WelsEncoderEncodeExt (sWelsEncCtx* pCtx, SFrameBSInfo* pFbi, const SSour
     pFbi->sLayerInfo[iNalIdx].iNalCount  = 0;
   }
   // perform csc/denoise/downsample/padding, generate spatial layers
-  iSpatialNum = pCtx->pVpp->BuildSpatialPicList (pCtx, pSrcPic);
-  if (iSpatialNum == -1) {
-    WelsLog (& (pCtx->sLogCtx), WELS_LOG_ERROR, "Failed in allocating memory in BuildSpatialPicList");
-    return ENC_RETURN_MEMALLOCERR;
+  int32_t iRet = pCtx->pVpp->BuildSpatialPicList (pCtx, pSrcPic, &iSpatialNum);
+  if (iRet != ENC_RETURN_SUCCESS) {
+    WelsLog (& (pCtx->sLogCtx), WELS_LOG_ERROR, "Failed in BuildSpatialPicList, error=%d", iRet);
+    return iRet;
   }

   if (pCtx->pFuncList->pfRc.pfWelsUpdateMaxBrWindowStatus) {
diff --git a/codec/encoder/core/src/wels_preprocess.cpp b/codec/encoder/core/src/wels_preprocess.cpp
index e9e81fc8..d3376974 100644
--- a/codec/encoder/core/src/wels_preprocess.cpp
+++ b/codec/encoder/core/src/wels_preprocess.cpp
@@ -56,9 +56,11 @@ namespace WelsEnc {
 int32_t WelsInitScaledPic (SWelsSvcCodingParam* pParam,  Scaled_Picture*  pScaledPic, CMemoryAlign* pMemoryAlign);
 bool  JudgeNeedOfScaling (SWelsSvcCodingParam* pParam, Scaled_Picture* pScaledPic);
 void    FreeScaledPic (Scaled_Picture*  pScaledPic, CMemoryAlign* pMemoryAlign);
-void  WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,  int32_t iDstStrideY, int32_t iDstStrideUV,
-                        uint8_t* pSrcY, uint8_t* pSrcU, uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideUV, int32_t iWidth,
-                        int32_t iHeight);
+void WelsMoveMemory_c(uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,
+                      int32_t iDstStrideY, int32_t iDstStrideU,
+                      int32_t iDstStrideV, uint8_t* pSrcY, uint8_t* pSrcU,
+                      uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideU,
+                      int32_t iSrcStrideV, int32_t iWidth, int32_t iHeight);

 //******* table definition ***********************************************************************//
 const uint8_t g_kuiRefTemporalIdx[MAX_TEMPORAL_LEVEL][MAX_GOP_SIZE] = {
@@ -215,18 +217,18 @@ void CWelsPreProcess::FreeSpatialPictures (sWelsEncCtx* pCtx) {
   }
 }

-int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePicture* kpSrcPic) {
+int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePicture* kpSrcPic, int32_t* pSpatialNum) {
   SWelsSvcCodingParam* pSvcParam = pCtx->pSvcParam;
-  int32_t iSpatialNum = 0;
   int32_t iWidth = ((kpSrcPic->iPicWidth >> 1) << 1);
   int32_t iHeight = ((kpSrcPic->iPicHeight >> 1) << 1);
+  *pSpatialNum = 0;

   if (!m_bInitDone) {
     if (WelsPreprocessCreate() != 0)
-      return -1;
+      return ENC_RETURN_MEMALLOCERR;

     if (WelsPreprocessReset (pCtx, iWidth, iHeight) != 0)
-      return -1;
+      return ENC_RETURN_MEMALLOCERR;

     m_iAvaliableRefInSpatialPicList = pSvcParam->iNumRefFrame;

@@ -234,18 +236,21 @@ int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePi
   } else {
     if ((iWidth != pSvcParam->SUsedPicRect.iWidth) || (iHeight != pSvcParam->SUsedPicRect.iHeight)) {
       if (WelsPreprocessReset (pCtx, iWidth, iHeight) != 0)
-        return -1;
+        return ENC_RETURN_MEMALLOCERR;
     }
   }

   if (m_pInterfaceVp == NULL)
-    return -1;
+    return ENC_RETURN_MEMALLOCERR;

   pCtx->pVaa->bSceneChangeFlag = pCtx->pVaa->bIdrPeriodFlag = false;

-  iSpatialNum = SingleLayerPreprocess (pCtx, kpSrcPic, &m_sScaledPicture);
+  int32_t iRet = SingleLayerPreprocess (pCtx, kpSrcPic, &m_sScaledPicture, pSpatialNum);
+  if (iRet != ENC_RETURN_SUCCESS) {
+    return iRet;
+  }

-  return iSpatialNum;
+  return ENC_RETURN_SUCCESS;
 }

 SPicture* CWelsPreProcess::GetBestRefPic (EUsageType iUsageType, bool bSceneLtr, EWelsSliceType eSliceType,
@@ -344,7 +349,7 @@ int32_t CWelsPreProcess::UpdateSpatialPictures (sWelsEncCtx* pCtx, SWelsSvcCodin
  *  @return: exact number of spatial layers need to encoder indeed
  */
 int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSourcePicture* kpSrc,
-    Scaled_Picture* pScaledPicture) {
+    Scaled_Picture* pScaledPicture, int32_t* pSpatialNum) {
   SWelsSvcCodingParam* pSvcParam    = pCtx->pSvcParam;
   int8_t  iDependencyId             = pSvcParam->iSpatialLayerNum - 1;

@@ -377,10 +382,13 @@ int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSource
     }
   }

+  *pSpatialNum = 0;
   pSrcPic = pScaledPicture->pScaledInputPicture ? pScaledPicture->pScaledInputPicture : GetCurrentOrigFrame (
               iDependencyId);
-
-  WelsMoveMemoryWrapper (pSvcParam, pSrcPic, kpSrc, iSrcWidth, iSrcHeight);
+  int32_t iRet = WelsMoveMemoryWrapper (pSvcParam, pSrcPic, kpSrc, iSrcWidth, iSrcHeight);
+  if (iRet != ENC_RETURN_SUCCESS) {
+    return iRet;
+  }

   if (pSvcParam->bEnableDenoise)
     BilateralDenoising (pSrcPic, iSrcWidth, iSrcHeight);
@@ -471,8 +479,8 @@ int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSource

     }
   }
-  return iSpatialNum;
-
+  *pSpatialNum = iSpatialNum;
+  return ENC_RETURN_SUCCESS;
 }


@@ -656,9 +664,11 @@ int32_t CWelsPreProcess::DownsamplePadding (SPicture* pSrc, SPicture* pDstPic,
     if (iSrcWidth != iShrinkWidth || iSrcHeight != iShrinkHeight) {
       iRet = m_pInterfaceVp->Process (iMethodIdx, &sSrcPixMap, &sDstPicMap);
     } else {
-      WelsMoveMemory_c (pDstPic->pData[0], pDstPic->pData[1], pDstPic->pData[2], pDstPic->iLineSize[0], pDstPic->iLineSize[1],
-                        pSrc->pData[0], pSrc->pData[1], pSrc->pData[2], pSrc->iLineSize[0], pSrc->iLineSize[1],
-                        iSrcWidth, iSrcHeight);
+      WelsMoveMemory_c(pDstPic->pData[0], pDstPic->pData[1], pDstPic->pData[2],
+                       pDstPic->iLineSize[0], pDstPic->iLineSize[1],
+                       pDstPic->iLineSize[2], pSrc->pData[0], pSrc->pData[1],
+                       pSrc->pData[2], pSrc->iLineSize[0], pSrc->iLineSize[1],
+                       pSrc->iLineSize[2], iSrcWidth, iSrcHeight);
     }
   } else {
     memcpy (&sDstPicMap, &sSrcPixMap, sizeof (sDstPicMap)); // confirmed_safe_unsafe_usage
@@ -1371,9 +1381,11 @@ void* WelsMemset (void* p, int32_t val, uint32_t uiSize) {
 }

 //i420_to_i420_c
-void  WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,  int32_t iDstStrideY, int32_t iDstStrideUV,
-                        uint8_t* pSrcY, uint8_t* pSrcU, uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideUV, int32_t iWidth,
-                        int32_t iHeight) {
+void WelsMoveMemory_c(uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,
+                      int32_t iDstStrideY, int32_t iDstStrideU,
+                      int32_t iDstStrideV, uint8_t* pSrcY, uint8_t* pSrcU,
+                      uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideU,
+                      int32_t iSrcStrideV, int32_t iWidth, int32_t iHeight) {
   int32_t   iWidth2 = iWidth >> 1;
   int32_t   iHeight2 = iHeight >> 1;
   int32_t   j;
@@ -1387,18 +1399,18 @@ void  WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,  int32_t
   for (j = iHeight2; j; j--) {
     WelsMemcpy (pDstU, pSrcU, iWidth2);
     WelsMemcpy (pDstV, pSrcV, iWidth2);
-    pDstU += iDstStrideUV;
-    pDstV += iDstStrideUV;
-    pSrcU += iSrcStrideUV;
-    pSrcV += iSrcStrideUV;
+    pDstU += iDstStrideU;
+    pDstV += iDstStrideV;
+    pSrcU += iSrcStrideU;
+    pSrcV += iSrcStrideV;
   }
 }

-void  CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic,
+int32_t  CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic,
     const SSourcePicture* kpSrc,
     const int32_t kiTargetWidth, const int32_t kiTargetHeight) {
   if (VIDEO_FORMAT_I420 != (kpSrc->iColorFormat & (~VIDEO_FORMAT_VFlip)))
-    return;
+    return ENC_RETURN_INVALIDINPUT;

   int32_t  iSrcWidth       = kpSrc->iPicWidth;
   int32_t  iSrcHeight      = kpSrc->iPicHeight;
@@ -1423,40 +1435,47 @@ void  CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SP
   uint8_t* pSrcU = kpSrc->pData[1] + iSrcOffset[1];
   uint8_t* pSrcV = kpSrc->pData[2] + iSrcOffset[2];
   const int32_t kiSrcStrideY = kpSrc->iStride[0];
-  const int32_t kiSrcStrideUV = kpSrc->iStride[1];
+  const int32_t kiSrcStrideU = kpSrc->iStride[1];
+  const int32_t kiSrcStrideV = kpSrc->iStride[2];

   uint8_t* pDstY = pDstPic->pData[0];
   uint8_t* pDstU = pDstPic->pData[1];
   uint8_t* pDstV = pDstPic->pData[2];
   const int32_t kiDstStrideY = pDstPic->iLineSize[0];
-  const int32_t kiDstStrideUV = pDstPic->iLineSize[1];
+  const int32_t kiDstStrideU = pDstPic->iLineSize[1];
+  const int32_t kiDstStrideV = pDstPic->iLineSize[2];

   if (pSrcY) {
     if (iSrcWidth <= 0 || iSrcHeight <= 0 || (iSrcWidth * iSrcHeight > (MAX_MBS_PER_FRAME << 8)))
-      return;
-    if (kiSrcTopOffsetY >= iSrcHeight || kiSrcLeftOffsetY >= iSrcWidth || iSrcWidth > kiSrcStrideY)
-      return;
+      return ENC_RETURN_INVALIDINPUT;
+    if (kiSrcTopOffsetY >= iSrcHeight || kiSrcLeftOffsetY >= iSrcWidth || iSrcWidth > kiSrcStrideY
+        || (iSrcWidth >> 1) > kiSrcStrideU || (iSrcWidth >> 1) > kiSrcStrideV)
+      return ENC_RETURN_INVALIDINPUT;
   }
   if (pDstY) {
     if (kiTargetWidth <= 0 || kiTargetHeight <= 0 || (kiTargetWidth * kiTargetHeight > (MAX_MBS_PER_FRAME << 8)))
-      return;
-    if (kiTargetWidth > kiDstStrideY)
-      return;
+      return ENC_RETURN_INVALIDINPUT;
+    if (kiTargetWidth > kiDstStrideY || (kiTargetWidth >> 1) > kiDstStrideU || (kiTargetWidth >> 1) > kiDstStrideV)
+      return ENC_RETURN_INVALIDINPUT;
   }

   if (pSrcY == NULL || pSrcU == NULL || pSrcV == NULL || pDstY == NULL || pDstU == NULL || pDstV == NULL
       || (iSrcWidth & 1) || (iSrcHeight & 1)) {
+    return ENC_RETURN_INVALIDINPUT;
   } else {
     //i420_to_i420_c
-    WelsMoveMemory_c (pDstY,  pDstU,  pDstV,  kiDstStrideY, kiDstStrideUV,
-                      pSrcY,  pSrcU,  pSrcV, kiSrcStrideY, kiSrcStrideUV, iSrcWidth, iSrcHeight);
+    WelsMoveMemory_c(pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideU,
+                     kiDstStrideV, pSrcY, pSrcU, pSrcV, kiSrcStrideY,
+                     kiSrcStrideU, kiSrcStrideV, iSrcWidth, iSrcHeight);

     //in VP Process
     if (kiTargetWidth > iSrcWidth || kiTargetHeight > iSrcHeight) {
-      Padding (pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideUV, iSrcWidth, kiTargetWidth, iSrcHeight, kiTargetHeight);
+      Padding(pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideU, iSrcWidth,
+              kiTargetWidth, iSrcHeight, kiTargetHeight);
     }
   }

+  return ENC_RETURN_SUCCESS;
 }

 bool CWelsPreProcess::GetSceneChangeFlag (ESceneChangeIdc eSceneChangeIdc) {
diff --git a/codec/encoder/plus/src/welsEncoderExt.cpp b/codec/encoder/plus/src/welsEncoderExt.cpp
index 7c85f232..aca94ecc 100644
--- a/codec/encoder/plus/src/welsEncoderExt.cpp
+++ b/codec/encoder/plus/src/welsEncoderExt.cpp
@@ -418,6 +418,10 @@ int CWelsH264SVCEncoder ::EncodeFrameInternal (const SSourcePicture*  pSrcPic, S
              kiEncoderReturn);
     WelsUninitEncoderExt (&m_pEncContext);
     return cmMallocMemeError;
+  } else if (kiEncoderReturn == ENC_RETURN_INVALIDINPUT) {
+    WelsLog (&m_pWelsTrace->m_sLogCtx, WELS_LOG_ERROR, "CWelsH264SVCEncoder::EncodeFrame() invalid input, err=%d",
+             kiEncoderReturn);
+    return cmUnsupportedData;
   } else if ((kiEncoderReturn != ENC_RETURN_SUCCESS) && (kiEncoderReturn == ENC_RETURN_CORRECTED)) {
     WelsLog (&m_pWelsTrace->m_sLogCtx, WELS_LOG_ERROR, "unexpected return(%d) from EncodeFrameInternal()!",
              kiEncoderReturn);
diff --git a/test/api/encoder_test.cpp b/test/api/encoder_test.cpp
index 94c085ed..69e877b5 100644
--- a/test/api/encoder_test.cpp
+++ b/test/api/encoder_test.cpp
@@ -4,6 +4,41 @@
 #include "utils/BufferedData.h"
 #include <string>
 #include <cstdlib>
+#include <cmath>
+
+static void GeneratePattern(uint8_t* yBuf, int yStride, uint8_t* uBuf, int uStride, uint8_t* vBuf, int vStride, int width, int height) {
+  for (int y = 0; y < height; ++y) {
+    for (int x = 0; x < width; ++x) {
+      if (x < yStride) {
+        yBuf[y * yStride + x] = (x * 4 + y * 4) % 256;
+      }
+    }
+  }
+  for (int y = 0; y < (height >> 1); ++y) {
+    for (int x = 0; x < (width >> 1); ++x) {
+      if (x < uStride) {
+        uBuf[y * uStride + x] = (x * 8) % 256;
+      }
+      if (x < vStride) {
+        vBuf[y * vStride + x] = (y * 8) % 256;
+      }
+    }
+  }
+}
+
+static double CalculatePlanePsnr(const uint8_t* ref, int refStride, const uint8_t* test, int testStride, int width, int height) {
+  double mse = 0;
+  int compareWidth = std::min(width, std::min(refStride, testStride));
+  for (int y = 0; y < height; ++y) {
+    for (int x = 0; x < compareWidth; ++x) {
+      double diff = ref[y * refStride + x] - test[y * testStride + x];
+      mse += diff * diff;
+    }
+  }
+  mse /= (compareWidth * height);
+  if (mse == 0) return 99.0;
+  return 10.0 * log10((255.0 * 255.0) / mse);
+}

 static void UpdateHashFromFrame (const SFrameBSInfo& info, SHA1Context* ctx) {
   for (int i = 0; i < info.iLayerNum; ++i) {
@@ -351,3 +386,176 @@ TEST_F(EncoderInitTest, ScreenContentScrollMotionVectorBounds) {
   rv = encoder_->EncodeFrame(&pic, &info);
   ASSERT_EQ(0, rv);
 }
+
+// This test verifies that the encoder correctly handles frames with distinct,
+// asymmetric strides for the U and V chroma planes (e.g., when U stride is much
+// larger than V stride, or vice versa). It ensures that memory copy routines
+// advance each chroma plane pointer by its own stride without invalid memory
+// access.
+TEST_F(EncoderInitTest, CustomChromaPlaneStrides) {
+  SEncParamExt param;
+  encoder_->GetDefaultParams(&param);
+
+  param.iUsageType = CAMERA_VIDEO_REAL_TIME;
+  param.iPicWidth = 64;
+  param.iPicHeight = 64;
+  param.fMaxFrameRate = 30.0f;
+  param.iSpatialLayerNum = 1;
+  param.iRCMode = RC_OFF_MODE;
+
+  param.sSpatialLayers[0].iVideoWidth = param.iPicWidth;
+  param.sSpatialLayers[0].iVideoHeight = param.iPicHeight;
+  param.sSpatialLayers[0].fFrameRate = param.fMaxFrameRate;
+  param.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
+  param.sSpatialLayers[0].iDLayerQp = 0;
+
+  int rv = encoder_->InitializeExt(&param);
+  ASSERT_EQ(0, rv);
+
+  // Initialize a decoder to verify correctness of the output
+  ISVCDecoder* decoder = nullptr;
+  rv = WelsCreateDecoder(&decoder);
+  ASSERT_EQ(0, rv);
+  ASSERT_TRUE(decoder != nullptr);
+
+  SDecodingParam decParam;
+  memset(&decParam, 0, sizeof(SDecodingParam));
+  decParam.uiTargetDqLayer = UCHAR_MAX;
+  decParam.eEcActiveIdc = ERROR_CON_SLICE_COPY;
+  decParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_DEFAULT;
+
+  rv = decoder->Initialize(&decParam);
+  ASSERT_EQ(0, rv);
+
+  SFrameBSInfo info;
+  memset(&info, 0, sizeof(SFrameBSInfo));
+
+  // Configure distinct strides: U stride is much larger than V stride.
+  int strideY = 64;
+  int strideU = 4096;
+  int strideV = 32;
+
+  // Generate a distinct pattern
+  std::vector<uint8_t> bufY(strideY * param.iPicHeight);
+  std::vector<uint8_t> bufU(strideU * (param.iPicHeight >> 1));
+  std::vector<uint8_t> bufV(strideV * (param.iPicHeight >> 1));
+  GeneratePattern(bufY.data(), strideY, bufU.data(), strideU, bufV.data(), strideV, param.iPicWidth, param.iPicHeight);
+
+  SSourcePicture pic;
+  memset(&pic, 0, sizeof(SSourcePicture));
+  pic.iPicWidth = param.iPicWidth;
+  pic.iPicHeight = param.iPicHeight;
+  pic.iColorFormat = videoFormatI420;
+  pic.iStride[0] = strideY;
+  pic.iStride[1] = strideU;
+  pic.iStride[2] = strideV;
+  pic.pData[0] = bufY.data();
+  pic.pData[1] = bufU.data();
+  pic.pData[2] = bufV.data();
+
+  rv = encoder_->EncodeFrame(&pic, &info);
+  ASSERT_EQ(0, rv);
+
+  // Calculate total bitstream size
+  int len = 0;
+  for (int i = 0; i < info.iLayerNum; ++i) {
+    const SLayerBSInfo& layerInfo = info.sLayerInfo[i];
+    for (int j = 0; j < layerInfo.iNalCount; ++j) {
+      len += layerInfo.pNalLengthInByte[j];
+    }
+  }
+  ASSERT_GT(len, 0);
+
+  // Decode the encoded frame
+  unsigned char* pData[3] = {nullptr};
+  SBufferInfo dstBufInfo;
+  memset(&dstBufInfo, 0, sizeof(SBufferInfo));
+
+  rv = decoder->DecodeFrame2(info.sLayerInfo[0].pBsBuf, len, pData, &dstBufInfo);
+  ASSERT_EQ(0, rv);
+
+  if (dstBufInfo.iBufferStatus == 0) {
+    rv = decoder->DecodeFrame2(nullptr, 0, pData, &dstBufInfo);
+    ASSERT_EQ(0, rv);
+  }
+
+  ASSERT_EQ(1, dstBufInfo.iBufferStatus);
+
+  // Verify that the decoded YUV content matches our original pattern (high PSNR)
+  int decodedWidthU = dstBufInfo.UsrData.sSystemBuffer.iWidth >> 1;
+  int decodedHeightU = dstBufInfo.UsrData.sSystemBuffer.iHeight >> 1;
+  int strideDecY = dstBufInfo.UsrData.sSystemBuffer.iStride[0];
+  int strideDecChroma = dstBufInfo.UsrData.sSystemBuffer.iStride[1];
+
+  uint8_t* decY = pData[0];
+  uint8_t* decU = pData[1];
+  uint8_t* decV = pData[2];
+
+  ASSERT_TRUE(decY != nullptr);
+  ASSERT_TRUE(decU != nullptr);
+  ASSERT_TRUE(decV != nullptr);
+
+  double psnrY = CalculatePlanePsnr(bufY.data(), strideY, decY, strideDecY, param.iPicWidth, param.iPicHeight);
+  double psnrU = CalculatePlanePsnr(bufU.data(), strideU, decU, strideDecChroma, decodedWidthU, decodedHeightU);
+  double psnrV = CalculatePlanePsnr(bufV.data(), strideV, decV, strideDecChroma, decodedWidthU, decodedHeightU);
+
+  // With lossless QP=0, PSNR should be extremely high (effectively identical)
+  EXPECT_GT(psnrY, 40.0);
+  EXPECT_GT(psnrU, 40.0);
+  EXPECT_GT(psnrV, 40.0);
+
+  WelsDestroyDecoder(decoder);
+}
+
+// This test verifies that the encoder safely returns early and avoids invalid
+// memory access (no crash / ASan error) when the input source picture has U or
+// V strides that are smaller than the required width/2.
+TEST_F(EncoderInitTest, CustomChromaPlaneStridesInvalidSrc) {
+  SEncParamExt param;
+  encoder_->GetDefaultParams(&param);
+
+  param.iUsageType = CAMERA_VIDEO_REAL_TIME;
+  param.iPicWidth = 64;
+  param.iPicHeight = 64;
+  param.fMaxFrameRate = 30.0f;
+  param.iSpatialLayerNum = 1;
+  param.iRCMode = RC_OFF_MODE;
+
+  param.sSpatialLayers[0].iVideoWidth = param.iPicWidth;
+  param.sSpatialLayers[0].iVideoHeight = param.iPicHeight;
+  param.sSpatialLayers[0].fFrameRate = param.fMaxFrameRate;
+  param.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
+  param.sSpatialLayers[0].iDLayerQp = 0;
+
+  int rv = encoder_->InitializeExt(&param);
+  ASSERT_EQ(0, rv);
+
+  SFrameBSInfo info;
+  memset(&info, 0, sizeof(SFrameBSInfo));
+
+  // Configure invalid strides: U stride is 16, which is smaller than width/2 (32).
+  int strideY = 64;
+  int strideU = 16; // Invalid: must be >= 32
+  int strideV = 32;
+
+  // Generate pattern (alternative pattern is not strictly needed but we'll use a simple fill)
+  std::vector<uint8_t> bufY(strideY * param.iPicHeight, 128);
+  std::vector<uint8_t> bufU(strideU * (param.iPicHeight >> 1), 100);
+  std::vector<uint8_t> bufV(strideV * (param.iPicHeight >> 1), 200);
+
+  SSourcePicture pic;
+  memset(&pic, 0, sizeof(SSourcePicture));
+  pic.iPicWidth = param.iPicWidth;
+  pic.iPicHeight = param.iPicHeight;
+  pic.iColorFormat = videoFormatI420;
+  pic.iStride[0] = strideY;
+  pic.iStride[1] = strideU;
+  pic.iStride[2] = strideV;
+  pic.pData[0] = bufY.data();
+  pic.pData[1] = bufU.data();
+  pic.pData[2] = bufV.data();
+
+  // This should reject the frame and return cmUnsupportedData
+  rv = encoder_->EncodeFrame(&pic, &info);
+  ASSERT_EQ(cmUnsupportedData, rv);
+}