diff --git a/AUTHORS b/AUTHORS index 0dff25990..e3557b631 100644 --- a/AUTHORS +++ b/AUTHORS @@ -82,6 +82,7 @@ L. E. Segovia ledoge Leo Izen Linus VandeVondele +LongYinan Lovell Fuller Lucas Chollet Maarten DB diff --git a/lib/extras/codec_test.cc b/lib/extras/codec_test.cc index e33eb3752..14ac14672 100644 --- a/lib/extras/codec_test.cc +++ b/lib/extras/codec_test.cc @@ -435,6 +435,49 @@ TEST(CodecTest, TestPNM) { ASSERT_TRUE(std::abs(d - -3.141592) < 1E-15); } +TEST(CodecTest, PNMSampleAboveMaxVal) { + const auto decode = [](const std::string& header, + const std::vector& samples) -> bool { + std::vector pnm(header.begin(), header.end()); + pnm.insert(pnm.end(), samples.begin(), samples.end()); + PackedPixelFile ppf; + return static_cast(DecodeBytes(Bytes(pnm), ColorHints(), &ppf)); + }; + const std::string kPam12 = + "P7\nWIDTH 1\nHEIGHT 1\nDEPTH 4\nMAXVAL 4095\n" + "TUPLTYPE RGB_ALPHA\nENDHDR\n"; + + // Samples up to MaxVal are valid. + EXPECT_TRUE(decode("P5\n2 1\n15\n", {0, 15})); + EXPECT_TRUE(decode("P5\n2 1\n255\n", {0, 255})); + EXPECT_TRUE(decode("P6\n1 1\n4095\n", {0x0F, 0xFF, 0, 0, 0x0F, 0xFF})); + EXPECT_TRUE(decode("P5\n1 1\n65535\n", {0xFF, 0xFF})); + EXPECT_TRUE(decode(kPam12, {0, 0, 0, 0, 0, 0, 0x0F, 0xFF})); + +#if (!JXL_CRASH_ON_ERROR) + // Samples above MaxVal are rejected. + EXPECT_FALSE(decode("P5\n2 1\n15\n", {0, 16})); + EXPECT_FALSE(decode("P6\n1 1\n4095\n", {0x0F, 0xFF, 0x10, 0x00, 0, 0})); + EXPECT_FALSE(decode("P6\n1 1\n4095\n", {0, 0, 0, 0, 0xFF, 0xFF})); + EXPECT_FALSE(decode(kPam12, {0, 0, 0, 0, 0, 0, 0x10, 0x00})); + // Check every sample position of images larger than 8 bytes. + const std::vector max4(19, 15); + std::vector max12; + for (size_t i = 0; i < 19; ++i) max12.insert(max12.end(), {0x0F, 0xFF}); + EXPECT_TRUE(decode("P5\n19 1\n15\n", max4)); + EXPECT_TRUE(decode("P5\n19 1\n4095\n", max12)); + for (size_t i = 0; i < 19; ++i) { + std::vector samples4 = max4; + samples4[i] = 16; + EXPECT_FALSE(decode("P5\n19 1\n15\n", samples4)) << i; + std::vector samples12 = max12; + samples12[2 * i] = 0x10; + samples12[2 * i + 1] = 0x00; + EXPECT_FALSE(decode("P5\n19 1\n4095\n", samples12)) << i; + } +#endif +} + TEST(CodecTest, FormatNegotiation) { const std::vector accepted_formats = { {/*num_channels=*/4, diff --git a/lib/extras/dec/pnm.cc b/lib/extras/dec/pnm.cc index 2b0badbbb..d7dd0ca68 100644 --- a/lib/extras/dec/pnm.cc +++ b/lib/extras/dec/pnm.cc @@ -340,6 +340,35 @@ class Parser { const uint8_t* const end_; }; +// Netpbm requires every sample to be in [0, MaxVal]. MaxVal is 2^bits - 1, so +// a sample is out of range iff it has a bit set at or above `bits_per_sample`. +// Samples are big-endian, so for 16-bit storage only the first byte of each +// sample can hold such bits. `data` holds `size` bytes of integer samples. +Status VerifySampleRange(const uint8_t* data, size_t size, + size_t bits_per_sample) { + // Every value of the storage type is a valid sample. + if (bits_per_sample == 8 || bits_per_sample == 16) return true; + // OR the input 8 bytes at a time: byte k of `acc` is then the OR of all + // bytes at offsets equal to k modulo 8. + uint64_t acc = 0; + size_t i = 0; + for (; i + 8 <= size; i += 8) { + uint64_t v; + memcpy(&v, data + i, 8); + acc |= v; + } + uint8_t acc_bytes[8]; + memcpy(acc_bytes, &acc, 8); + for (; i < size; ++i) acc_bytes[i % 8] |= data[i]; + const size_t step = bits_per_sample > 8 ? 2 : 1; + const size_t shift = + bits_per_sample > 8 ? bits_per_sample - 8 : bits_per_sample; + uint8_t bits = 0; + for (size_t k = 0; k < 8; k += step) bits |= acc_bytes[k]; + if (bits >> shift) return JXL_FAILURE("PNM: sample value exceeds MaxVal"); + return true; +} + } // namespace struct PNMChunkedInputFrame { @@ -432,6 +461,9 @@ StatusOr ChunkedPNMDecoder::Init( if (size < required_size) { return JXL_FAILURE("PNM file too small"); } + JXL_RETURN_IF_ERROR(VerifySampleRange(dec.pnm_.data() + dec.data_start_, + required_size - dec.data_start_, + header.bits_per_sample)); return dec; } @@ -555,6 +587,10 @@ Status DecodeImagePNM(const Span bytes, if (pnm_remaining_size < required_pnm_size) { return JXL_FAILURE("PNM file too small"); } + if (!header.floating_point) { + JXL_RETURN_IF_ERROR( + VerifySampleRange(pos, required_pnm_size, header.bits_per_sample)); + } ppf->frames.clear(); {