Commit a749f280b3 for aom

commit a749f280b37306a2b64557ffaf6f47a4e14e3f42
Author: James Zern <jzern@google.com>
Date:   Sat Mar 14 18:21:45 2026 -0700

    av1 encoder: reject unequal UV strides

    The `aom_image_t` from the external API is converted to an internal
    `YV12_BUFFER_CONFIG`, which assumes U and V strides are the same. This
    avoids a potential crash or corrupted chroma if they differ.

    Ported from libvpx:
      915643b94 vp[89] encoders: reject unequal UV strides

    Bug: 492213293
    Change-Id: I3c878bdd330a8f7f838ac3bce8b66201c49f537d

diff --git a/aom/aom_image.h b/aom/aom_image.h
index 3b71086f61..7d0e9fcf3a 100644
--- a/aom/aom_image.h
+++ b/aom/aom_image.h
@@ -229,8 +229,13 @@ typedef struct aom_image {
   /* planes[AOM_PLANE_V] = NULL and stride[AOM_PLANE_V] = 0 when fmt ==
    * AOM_IMG_FMT_NV12 */
   unsigned char *planes[3]; /**< pointer to the top left pixel for each plane */
-  int stride[3];            /**< stride between rows for each plane */
-  size_t sz;                /**< data size */
+  /*!Stride between rows for each plane
+   *
+   * \note With planar formats, \c stride[AOM_PLANE_U] must be the same as \c
+   * stride[AOM_PLANE_V].
+   */
+  int stride[3];
+  size_t sz; /**< data size */

   int bps; /**< bits per sample (for packed formats) */

diff --git a/av1/av1_cx_iface.c b/av1/av1_cx_iface.c
index 5abd959d91..443f62c261 100644
--- a/av1/av1_cx_iface.c
+++ b/av1/av1_cx_iface.c
@@ -989,6 +989,10 @@ static aom_codec_err_t validate_img(aom_codec_alg_priv_t *ctx,

   if (img->d_w != ctx->cfg.g_w || img->d_h != ctx->cfg.g_h)
     ERROR("Image size must match encoder init configuration size");
+  assert(img->fmt & AOM_IMG_FMT_PLANAR);
+  if (!ctx->cfg.monochrome && img->fmt != AOM_IMG_FMT_NV12 &&
+      img->stride[AOM_PLANE_U] != img->stride[AOM_PLANE_V])
+    ERROR("Image U/V strides must match");

   // 6.4.2 Color config semantics
   // If matrix_coefficients is equal to MC_IDENTITY, it is a requirement of
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index af86bb8315..06e4048898 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -9,12 +9,14 @@
  * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
  */

+#include <array>
 #include <cassert>
 #include <climits>
 #include <cmath>
 #include <cstdint>
 #include <cstdlib>
 #include <cstring>
+#include <memory>
 #include <tuple>

 #include "gtest/gtest.h"
@@ -190,6 +192,107 @@ TEST(EncodeAPI, InvalidControlId) {
   EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&enc));
 }

+TEST(EncodeAPI, InvalidUVStrides) {
+  static constexpr std::array<aom_img_fmt_t, 11> kAv1ImageFormats = {
+    AOM_IMG_FMT_YV12,    AOM_IMG_FMT_I420,   AOM_IMG_FMT_AOMYV12,
+    AOM_IMG_FMT_AOMI420, AOM_IMG_FMT_I422,   AOM_IMG_FMT_I444,
+    AOM_IMG_FMT_NV12,    AOM_IMG_FMT_I42016, AOM_IMG_FMT_YV1216,
+    AOM_IMG_FMT_I42216,  AOM_IMG_FMT_I44416
+  };
+  struct UVStride {
+    int u_stride;
+    int v_stride;
+  };
+  constexpr int kWidth = 64;
+  constexpr int kHeight = 64;
+  static constexpr std::array<UVStride, 4> kUVStrides = {
+    UVStride{ kWidth, kWidth - 1 }, UVStride{ kWidth, kWidth + 1 },
+    UVStride{ kWidth - 1, kWidth }, UVStride{ kWidth + 1, kWidth }
+  };
+  aom_image_t img;
+  aom_codec_ctx_t enc;
+  aom_codec_enc_cfg_t cfg;
+  // Allocate a buffer large enough for a non-subsampled, high bitdepth image.
+  auto buf = std::make_unique<uint8_t[]>(kWidth * kHeight * 3 * 2);
+
+  ASSERT_EQ(aom_codec_enc_config_default(aom_codec_av1_cx(), &cfg,
+                                         /*usage=*/AOM_USAGE_REALTIME),
+            AOM_CODEC_OK);
+
+  for (const auto img_fmt : kAv1ImageFormats) {
+    const bool high_bit_depth =
+        (img_fmt & AOM_IMG_FMT_HIGHBITDEPTH) == AOM_IMG_FMT_HIGHBITDEPTH;
+    if (high_bit_depth && !(aom_codec_get_caps(aom_codec_av1_cx()) &
+                            AOM_CODEC_CAP_HIGHBITDEPTH)) {
+      break;
+    }
+    ASSERT_EQ(aom_img_wrap(&img, img_fmt, kWidth, kHeight, /*stride_align=*/1,
+                           buf.get()),
+              &img)
+        << "Unable to wrap aom_image for format: " << img_fmt;
+    const bool is_444 = (img.x_chroma_shift == 0 && img.y_chroma_shift == 0);
+    const bool is_422 = (img.x_chroma_shift == 1 && img.y_chroma_shift == 0);
+    // 4:4:4 is allowed in profile 1, 4:2:2 in profile 2.
+    cfg.g_profile = is_444 + 2 * is_422;
+    cfg.g_w = kWidth;
+    cfg.g_h = kHeight;
+    cfg.g_bit_depth = high_bit_depth ? AOM_BITS_10 : AOM_BITS_8;
+    // Monochrome is not allowed in profile 1.
+    for (cfg.monochrome = 0; cfg.monochrome <= (cfg.g_profile != 1);
+         ++cfg.monochrome) {
+      ASSERT_EQ(
+          aom_codec_enc_init(&enc, aom_codec_av1_cx(), &cfg,
+                             high_bit_depth ? AOM_CODEC_USE_HIGHBITDEPTH : 0),
+          AOM_CODEC_OK)
+          << " high_bit_depth: " << high_bit_depth;
+
+      for (const auto uv_stride : kUVStrides) {
+        const UVStride orig = { img.stride[AOM_PLANE_U],
+                                img.stride[AOM_PLANE_V] };
+        img.stride[AOM_PLANE_U] = uv_stride.u_stride;
+        img.stride[AOM_PLANE_V] =
+            (img_fmt == AOM_IMG_FMT_NV12) ? 0 : uv_stride.v_stride;
+        img.monochrome = cfg.monochrome;
+        // Monochrome should ignore the U and V planes and NV12 only sets one
+        // stride value, they should always succeed. The AOM* aom_img_fmt_t
+        // variants are unsupported by the encoder.
+        aom_codec_err_t expected_err =
+            ((cfg.monochrome && img_fmt != AOM_IMG_FMT_AOMYV12 &&
+              img_fmt != AOM_IMG_FMT_AOMI420) ||
+             img_fmt == AOM_IMG_FMT_NV12)
+                ? AOM_CODEC_OK
+                : AOM_CODEC_INVALID_PARAM;
+        EXPECT_EQ(aom_codec_encode(&enc, &img, /*pts=*/0, /*duration=*/1,
+                                   /*flags=*/0),
+                  expected_err)
+            << "Error: " << aom_codec_error_detail(&enc)
+            << ", format: " << img_fmt << ", U stride: " << uv_stride.u_stride
+            << ", V stride: " << uv_stride.v_stride;
+
+        // Ensure the encoder can recover when given valid strides.
+        img.stride[AOM_PLANE_U] = orig.u_stride;
+        img.stride[AOM_PLANE_V] = orig.v_stride;
+        expected_err =
+            (img_fmt == AOM_IMG_FMT_AOMYV12 || img_fmt == AOM_IMG_FMT_AOMI420)
+                ? AOM_CODEC_INVALID_PARAM
+                : AOM_CODEC_OK;
+        EXPECT_EQ(aom_codec_encode(&enc, &img, /*pts=*/0, /*duration=*/1,
+                                   /*flags=*/0),
+                  expected_err)
+            << "Error: " << aom_codec_error_detail(&enc)
+            << ", format: " << img_fmt << ", U stride: " << orig.u_stride
+            << ", V stride: " << orig.v_stride;
+      }
+
+      EXPECT_EQ(
+          aom_codec_encode(&enc, /*img=*/nullptr, /*pts=*/0, /*duration=*/0,
+                           /*flags=*/0),
+          AOM_CODEC_OK);
+      EXPECT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+    }
+  }
+}
+
 void EncodeSetSFrameOnFirstFrame(aom_img_fmt fmt, aom_codec_flags_t flag) {
   constexpr int kWidth = 2;
   constexpr int kHeight = 128;