Return an error when tone mapping a gain map that was not decoded (#3377)

avifRGBImageApplyGainMap() dereferences gainMap->image without checking
it for null. With the default avifDecoder::imageContentToDecode, the
gain map metadata is read but gainMap->image is left null, so tone
mapping a normally decoded image through avifImageApplyGainMap() crashes
(null pointer dereference, CWE-476).

Reject a missing gain map image upfront, next to the other input
validation, and return AVIF_RESULT_INVALID_ARGUMENT, as
avifRGBImageComputeGainMap() already does for the same situation. The
gain map is then consistently rejected, including calls that would not
need its pixels (e.g. a target headroom equal to the base headroom).

---------

Co-authored-by: krishna28238-arch <krishna28238-arch@users.noreply.github.com>
diff --git a/src/gainmap.c b/src/gainmap.c
index 1b3df43..a8d6288 100644
--- a/src/gainmap.c
+++ b/src/gainmap.c
@@ -91,6 +91,14 @@
         avifDiagnosticsPrintf(diag, "NULL input image");
         return AVIF_RESULT_INVALID_ARGUMENT;
     }
+    // The gain map image may be missing if it was not decoded (see
+    // avifDecoder::imageContentToDecode) or was never set. Fail early and
+    // consistently, even for calls that would not need the gain map pixels
+    // (e.g. a target headroom matching the base image headroom).
+    if (gainMap->image == NULL) {
+        avifDiagnosticsPrintf(diag, "gainMap->image is null (gain map image not decoded?)");
+        return AVIF_RESULT_INVALID_ARGUMENT;
+    }
     AVIF_CHECKRES(avifGainMapValidateMetadata(gainMap, diag));
 
     const uint32_t width = baseImage->width;
diff --git a/tests/gtest/avifgainmaptest.cc b/tests/gtest/avifgainmaptest.cc
index 71f8234..a26b8b3 100644
--- a/tests/gtest/avifgainmaptest.cc
+++ b/tests/gtest/avifgainmaptest.cc
@@ -1323,6 +1323,47 @@
   avifRGBImageFreePixels(&tone_mapped);
 }
 
+// Tone mapping needs the gain map pixel data. With the default
+// avifDecoder::imageContentToDecode, the gain map metadata is read but
+// gainMap->image is left null. Tone mapping such an image should return an
+// error rather than dereference a null pointer.
+TEST(ToneMapTest, ToneMapWithoutDecodedGainMap) {
+  ImagePtr image(avifImageCreateEmpty());
+  ASSERT_NE(image, nullptr);
+  DecoderPtr decoder(avifDecoderCreate());
+  ASSERT_NE(decoder, nullptr);
+  // Default imageContentToDecode: the gain map metadata is read but the gain
+  // map image is not decoded.
+  const avifResult result = avifDecoderReadFile(
+      decoder.get(), image.get(),
+      (std::string(data_path) + "seine_sdr_gainmap_srgb.avif").c_str());
+  ASSERT_EQ(result, AVIF_RESULT_OK)
+      << avifResultToString(result) << ": " << decoder->diag.error;
+  ASSERT_NE(image->gainMap, nullptr);
+  ASSERT_EQ(image->gainMap->image, nullptr);
+
+  // The check happens during input validation, before the headrooms are
+  // used, so any target headroom fails. Keep the headrooms of the original
+  // crash repro (the null dereference used to happen at the first use of
+  // gainMap->image).
+  image->gainMap->baseHdrHeadroom = {0, 1};
+  image->gainMap->alternateHdrHeadroom = {2, 1};
+
+  avifDiagnostics diag;
+  avifDiagnosticsClearError(&diag);
+
+  avifRGBImage tone_mapped = {};
+  tone_mapped.depth = 8;
+  tone_mapped.format = AVIF_RGB_FORMAT_RGBA;
+  EXPECT_EQ(
+      avifImageApplyGainMap(image.get(), image->gainMap,
+                            /*hdrHeadroom=*/1.0f, AVIF_COLOR_PRIMARIES_BT709,
+                            AVIF_TRANSFER_CHARACTERISTICS_SRGB, &tone_mapped,
+                            /*clli=*/nullptr, &diag),
+      AVIF_RESULT_INVALID_ARGUMENT)
+      << diag.error;
+}
+
 TEST(GainMapTest, OpaqueProperties) {
   ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
   ASSERT_NE(image, nullptr);