Reject 'sato' items with no input image items (#3237)
diff --git a/src/read.c b/src/read.c
index a6a33d8..2bc7696 100644
--- a/src/read.c
+++ b/src/read.c
@@ -6347,6 +6347,11 @@
++data->sampleTransformNumInputImageItems;
}
}
+ // A 'sato' derived image item must reference at least one input image item via 'iref dimg'.
+ if (data->sampleTransformNumInputImageItems == 0) {
+ avifDiagnosticsPrintf(data->diag, "Box[sato] item %u has no input image items", sampleTransformItem->id);
+ return AVIF_RESULT_BMFF_PARSE_FAILED;
+ }
// Check max number of input items allowed by the format.
if (data->sampleTransformNumInputImageItems > 32) {
avifDiagnosticsPrintf(data->diag,
diff --git a/tests/gtest/avifaltrtest.cc b/tests/gtest/avifaltrtest.cc
index 17575cc..f8b4c72 100644
--- a/tests/gtest/avifaltrtest.cc
+++ b/tests/gtest/avifaltrtest.cc
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: BSD-2-Clause
#include <algorithm>
+#include <cstring>
#include <iostream>
#include <string>
@@ -100,6 +101,49 @@
AVIF_RESULT_NO_CONTENT);
}
+// A 'sato' derived image item that references zero input image items must be
+// rejected at parse time. Otherwise it would reach an AVIF_ASSERT_OR_RETURN in
+// avifDecoderApplySampleTransformForPlanesImpl() and surface as
+// AVIF_RESULT_INTERNAL_ERROR.
+TEST(AltrTest, SampleTransformZeroInputItemsRejected) {
+ const std::string file_path =
+ std::string(data_path) + "weld_sato_12B_8B_q0.avif";
+ testutil::AvifRwData encoded = testutil::ReadFile(file_path);
+ ASSERT_GE(encoded.size, size_t{0x7f1 + 17});
+
+ // Sanity check: the fixture is the version this test was authored against.
+ ASSERT_EQ(std::memcmp(encoded.data + 0x111, "dimg", 4), 0);
+ ASSERT_EQ(encoded.data[0x7f1], 0x02);
+ ASSERT_EQ(encoded.data[0x7f2], 0x07);
+
+ // Demote the iref's inner reference type from 'dimg' to a type libavif does
+ // not interpret, so no item gets dimgForID == sato.id (i.e. the 'sato' item
+ // ends up with zero input image items).
+ std::memcpy(encoded.data + 0x111, "xxxx", 4);
+ // Rewrite the 17-byte 'sato' payload to a constants-only expression that
+ // passes avifSampleTransformExpressionIsValid(_, /*numInputImageItems=*/0):
+ // token_count = 11, [CONSTANT(16), NEGATION x 10] -> stack stays at 1.
+ encoded.data[0x7f2] = 0x0b;
+ encoded.data[0x7f3] = 0x00;
+ encoded.data[0x7f4] = 0x00;
+ encoded.data[0x7f5] = 0x00;
+ encoded.data[0x7f6] = 0x00;
+ encoded.data[0x7f7] = 0x10;
+ for (int i = 0; i < 10; ++i) {
+ encoded.data[0x7f8 + i] = 0x40;
+ }
+
+ DecoderPtr decoder(avifDecoderCreate());
+ ASSERT_NE(decoder, nullptr);
+ decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
+ ImagePtr image(avifImageCreateEmpty());
+ ASSERT_NE(image, nullptr);
+ const avifResult result = avifDecoderReadMemory(decoder.get(), image.get(),
+ encoded.data, encoded.size);
+ EXPECT_EQ(result, AVIF_RESULT_BMFF_PARSE_FAILED);
+ EXPECT_NE(result, AVIF_RESULT_INTERNAL_ERROR);
+}
+
//------------------------------------------------------------------------------
} // namespace