1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 02:37:35 +00:00

LibGfx/PNG: Make chunk-related functions fallible

This commit is contained in:
Lucas CHOLLET 2023-06-11 19:14:59 -04:00 committed by Andreas Kling
parent 96a1a8512f
commit 07b6c2ebfc

View file

@ -277,7 +277,7 @@ private:
size_t m_size_remaining { 0 }; size_t m_size_remaining { 0 };
}; };
static bool process_chunk(Streamer&, PNGLoadingContext& context); static ErrorOr<void> process_chunk(Streamer&, PNGLoadingContext& context);
union [[gnu::packed]] Pixel { union [[gnu::packed]] Pixel {
ARGB32 rgba { 0 }; ARGB32 rgba { 0 };
@ -624,7 +624,7 @@ static bool decode_png_size(PNGLoadingContext& context)
Streamer streamer(context.data_current_ptr, data_remaining); Streamer streamer(context.data_current_ptr, data_remaining);
while (!streamer.at_end() && !context.has_seen_iend) { while (!streamer.at_end() && !context.has_seen_iend) {
if (!process_chunk(streamer, context)) { if (auto result = process_chunk(streamer, context); result.is_error()) {
context.state = PNGLoadingContext::State::Error; context.state = PNGLoadingContext::State::Error;
return false; return false;
} }
@ -654,7 +654,7 @@ static bool decode_png_image_data_chunk(PNGLoadingContext& context)
Streamer streamer(context.data_current_ptr, data_remaining); Streamer streamer(context.data_current_ptr, data_remaining);
while (!streamer.at_end() && !context.has_seen_iend) { while (!streamer.at_end() && !context.has_seen_iend) {
if (!process_chunk(streamer, context)) { if (auto result = process_chunk(streamer, context); result.is_error()) {
context.state = PNGLoadingContext::State::Error; context.state = PNGLoadingContext::State::Error;
return false; return false;
} }
@ -683,7 +683,7 @@ static bool decode_png_animation_data_chunks(PNGLoadingContext& context, u32 req
Streamer streamer(context.data_current_ptr, data_remaining); Streamer streamer(context.data_current_ptr, data_remaining);
while (!streamer.at_end() && !context.has_seen_iend) { while (!streamer.at_end() && !context.has_seen_iend) {
if (!process_chunk(streamer, context)) { if (auto result = process_chunk(streamer, context); result.is_error()) {
context.state = PNGLoadingContext::State::Error; context.state = PNGLoadingContext::State::Error;
return false; return false;
} }
@ -717,7 +717,7 @@ static bool decode_png_chunks(PNGLoadingContext& context)
Streamer streamer(context.data_current_ptr, data_remaining); Streamer streamer(context.data_current_ptr, data_remaining);
while (!streamer.at_end() && !context.has_seen_iend) { while (!streamer.at_end() && !context.has_seen_iend) {
if (!process_chunk(streamer, context)) { if (auto result = process_chunk(streamer, context); result.is_error()) {
// Ignore failed chunk and just consider chunk decoding being done. // Ignore failed chunk and just consider chunk decoding being done.
// decode_png_bitmap() will check whether we got all required ones anyway. // decode_png_bitmap() will check whether we got all required ones anyway.
break; break;
@ -949,25 +949,26 @@ static bool is_valid_filter_method(u8 filter_method)
return filter_method == 0; return filter_method == 0;
} }
static bool process_IHDR(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_IHDR(ReadonlyBytes data, PNGLoadingContext& context)
{ {
if (data.size() < (int)sizeof(PNG_IHDR)) if (data.size() < (int)sizeof(PNG_IHDR))
return false; return Error::from_string_literal("IHDR chunk has an abnormal size");
auto& ihdr = *(const PNG_IHDR*)data.data();
auto const& ihdr = *(const PNG_IHDR*)data.data();
if (ihdr.width > maximum_width_for_decoded_images || ihdr.height > maximum_height_for_decoded_images) { if (ihdr.width > maximum_width_for_decoded_images || ihdr.height > maximum_height_for_decoded_images) {
dbgln("This PNG is too large for comfort: {}x{}", (u32)ihdr.width, (u32)ihdr.height); dbgln("This PNG is too large for comfort: {}x{}", (u32)ihdr.width, (u32)ihdr.height);
return false; return Error::from_string_literal("This PNG is too large for comfort");
} }
if (!is_valid_compression_method(ihdr.compression_method)) { if (!is_valid_compression_method(ihdr.compression_method)) {
dbgln("PNG has invalid compression method {}", ihdr.compression_method); dbgln("PNG has invalid compression method {}", ihdr.compression_method);
return false; return Error::from_string_literal("Unsupported compression method");
} }
if (!is_valid_filter_method(ihdr.filter_method)) { if (!is_valid_filter_method(ihdr.filter_method)) {
dbgln("PNG has invalid filter method {}", ihdr.filter_method); dbgln("PNG has invalid filter method {}", ihdr.filter_method);
return false; return Error::from_string_literal("Unsupported filter method");
} }
context.width = ihdr.width; context.width = ihdr.width;
@ -986,169 +987,169 @@ static bool process_IHDR(ReadonlyBytes data, PNGLoadingContext& context)
if (context.interlace_method != PngInterlaceMethod::Null && context.interlace_method != PngInterlaceMethod::Adam7) { if (context.interlace_method != PngInterlaceMethod::Null && context.interlace_method != PngInterlaceMethod::Adam7) {
dbgln_if(PNG_DEBUG, "PNGLoader::process_IHDR: unknown interlace method: {}", context.interlace_method); dbgln_if(PNG_DEBUG, "PNGLoader::process_IHDR: unknown interlace method: {}", context.interlace_method);
return false; return Error::from_string_literal("Unsupported interlacing method");
} }
switch (context.color_type) { switch (context.color_type) {
case PNG::ColorType::Greyscale: case PNG::ColorType::Greyscale:
if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8 && context.bit_depth != 16) if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8 && context.bit_depth != 16)
return false; return Error::from_string_literal("Unsupported bit depth for a greyscale image");
context.channels = 1; context.channels = 1;
break; break;
case PNG::ColorType::GreyscaleWithAlpha: case PNG::ColorType::GreyscaleWithAlpha:
if (context.bit_depth != 8 && context.bit_depth != 16) if (context.bit_depth != 8 && context.bit_depth != 16)
return false; return Error::from_string_literal("Unsupported bit depth for a greyscale image with alpha");
context.channels = 2; context.channels = 2;
break; break;
case PNG::ColorType::Truecolor: case PNG::ColorType::Truecolor:
if (context.bit_depth != 8 && context.bit_depth != 16) if (context.bit_depth != 8 && context.bit_depth != 16)
return false; return Error::from_string_literal("Unsupported bit depth for a true color image");
context.channels = 3; context.channels = 3;
break; break;
case PNG::ColorType::IndexedColor: case PNG::ColorType::IndexedColor:
if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8) if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8)
return false; return Error::from_string_literal("Unsupported bit depth for a indexed color image");
context.channels = 1; context.channels = 1;
break; break;
case PNG::ColorType::TruecolorWithAlpha: case PNG::ColorType::TruecolorWithAlpha:
if (context.bit_depth != 8 && context.bit_depth != 16) if (context.bit_depth != 8 && context.bit_depth != 16)
return false; return Error::from_string_literal("Unsupported bit depth for a true color image with alpha");
context.channels = 4; context.channels = 4;
break; break;
default: default:
return false; return Error::from_string_literal("Unsupported color type");
} }
return true; return {};
} }
static bool process_IDAT(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_IDAT(ReadonlyBytes data, PNGLoadingContext& context)
{ {
context.compressed_data.append(data.data(), data.size()); context.compressed_data.append(data.data(), data.size());
if (context.state < PNGLoadingContext::State::ImageDataChunkDecoded) if (context.state < PNGLoadingContext::State::ImageDataChunkDecoded)
context.state = PNGLoadingContext::State::ImageDataChunkDecoded; context.state = PNGLoadingContext::State::ImageDataChunkDecoded;
return true; return {};
} }
static bool process_PLTE(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_PLTE(ReadonlyBytes data, PNGLoadingContext& context)
{ {
context.palette_data.append((PaletteEntry const*)data.data(), data.size() / 3); TRY(context.palette_data.try_append((PaletteEntry const*)data.data(), data.size() / 3));
return true; return {};
} }
static bool process_tRNS(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_tRNS(ReadonlyBytes data, PNGLoadingContext& context)
{ {
switch (context.color_type) { switch (context.color_type) {
case PNG::ColorType::Greyscale: case PNG::ColorType::Greyscale:
case PNG::ColorType::Truecolor: case PNG::ColorType::Truecolor:
case PNG::ColorType::IndexedColor: case PNG::ColorType::IndexedColor:
context.palette_transparency_data.append(data.data(), data.size()); TRY(context.palette_transparency_data.try_append(data.data(), data.size()));
break; break;
default: default:
break; break;
} }
return true; return {};
} }
static bool process_cHRM(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_cHRM(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#11cHRM // https://www.w3.org/TR/png/#11cHRM
if (data.size() != 32) if (data.size() != 32)
return false; return Error::from_string_literal("cHRM chunk has an abnormal size");
context.chromaticities_and_whitepoint = *bit_cast<ChromaticitiesAndWhitepoint* const>(data.data()); context.chromaticities_and_whitepoint = *bit_cast<ChromaticitiesAndWhitepoint* const>(data.data());
return true; return {};
} }
static bool process_cICP(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_cICP(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#cICP-chunk // https://www.w3.org/TR/png/#cICP-chunk
if (data.size() != 4) if (data.size() != 4)
return false; return Error::from_string_literal("cICP chunk has an abnormal size");
context.coding_independent_code_points = *bit_cast<CodingIndependentCodePoints* const>(data.data()); context.coding_independent_code_points = *bit_cast<CodingIndependentCodePoints* const>(data.data());
return true; return {};
} }
static bool process_iCCP(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_iCCP(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#11iCCP // https://www.w3.org/TR/png/#11iCCP
size_t profile_name_length_max = min(80u, data.size()); size_t profile_name_length_max = min(80u, data.size());
size_t profile_name_length = strnlen((char const*)data.data(), profile_name_length_max); size_t profile_name_length = strnlen((char const*)data.data(), profile_name_length_max);
if (profile_name_length == 0 || profile_name_length == profile_name_length_max) if (profile_name_length == 0 || profile_name_length == profile_name_length_max)
return false; return Error::from_string_literal("iCCP chunk does not contain a profile name");
if (data.size() < profile_name_length + 2) if (data.size() < profile_name_length + 2)
return false; return Error::from_string_literal("iCCP chunk is too small");
u8 compression_method = data[profile_name_length + 1]; u8 compression_method = data[profile_name_length + 1];
if (compression_method != 0) if (compression_method != 0)
return false; return Error::from_string_literal("Unsupported compression method in the iCCP chunk");
context.embedded_icc_profile = EmbeddedICCProfile { { data.data(), profile_name_length }, data.slice(profile_name_length + 2) }; context.embedded_icc_profile = EmbeddedICCProfile { { data.data(), profile_name_length }, data.slice(profile_name_length + 2) };
return true; return {};
} }
static bool process_gAMA(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_gAMA(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#11gAMA // https://www.w3.org/TR/png/#11gAMA
if (data.size() != 4) if (data.size() != 4)
return false; return Error::from_string_literal("gAMA chunk has an abnormal size");
u32 gamma = *bit_cast<NetworkOrdered<u32> const*>(data.data()); u32 gamma = *bit_cast<NetworkOrdered<u32> const*>(data.data());
if (gamma & 0x8000'0000) if (gamma & 0x8000'0000)
return false; return Error::from_string_literal("Gamma value is too high");
context.gamma = gamma; context.gamma = gamma;
return true; return {};
} }
static bool process_sRGB(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_sRGB(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#srgb-standard-colour-space // https://www.w3.org/TR/png/#srgb-standard-colour-space
if (data.size() != 1) if (data.size() != 1)
return false; return Error::from_string_literal("sRGB chunk has an abnormal size");
u8 rendering_intent = data[0]; u8 rendering_intent = data[0];
if (rendering_intent > 3) if (rendering_intent > 3)
return false; return Error::from_string_literal("Unsupported rendering intent");
context.sRGB_rendering_intent = (RenderingIntent)rendering_intent; context.sRGB_rendering_intent = (RenderingIntent)rendering_intent;
return true; return {};
} }
static bool process_acTL(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_acTL(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#acTL-chunk // https://www.w3.org/TR/png/#acTL-chunk
if (context.has_seen_idat_chunk) if (context.has_seen_idat_chunk)
return true; // Ignore if we encounter it after the first idat return {}; // Ignore if we encounter it after the first idat
if (data.size() != sizeof(acTL_Chunk)) if (data.size() != sizeof(acTL_Chunk))
return false; return Error::from_string_literal("acTL chunk has an abnormal size");
auto const& acTL = *bit_cast<acTL_Chunk* const>(data.data()); auto const& acTL = *bit_cast<acTL_Chunk* const>(data.data());
context.animation_frame_count = acTL.num_frames; context.animation_frame_count = acTL.num_frames;
context.animation_loop_count = acTL.num_plays; context.animation_loop_count = acTL.num_plays;
context.has_seen_actl_chunk_before_idat = true; context.has_seen_actl_chunk_before_idat = true;
context.animation_frames.ensure_capacity(context.animation_frame_count); TRY(context.animation_frames.try_ensure_capacity(context.animation_frame_count));
return true; return {};
} }
static bool process_fcTL(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_fcTL(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#fcTL-chunk // https://www.w3.org/TR/png/#fcTL-chunk
if (!context.has_seen_actl_chunk_before_idat) if (!context.has_seen_actl_chunk_before_idat)
return true; // Ignore if it's not a valid animated png return {}; // Ignore if it's not a valid animated png
if (data.size() != sizeof(fcTL_Chunk)) if (data.size() != sizeof(fcTL_Chunk))
return false; return Error::from_string_literal("fcTL chunk has an abnormal size");
auto const& fcTL = *bit_cast<fcTL_Chunk* const>(data.data()); auto const& fcTL = *bit_cast<fcTL_Chunk* const>(data.data());
if (fcTL.sequence_number != context.animation_next_expected_seq) if (fcTL.sequence_number != context.animation_next_expected_seq)
return false; return Error::from_string_literal("Unexpected sequence number");
context.animation_next_expected_seq++; context.animation_next_expected_seq++;
if (fcTL.width == 0 || fcTL.height == 0) if (fcTL.width == 0 || fcTL.height == 0)
return false; return Error::from_string_literal("width and height must be greater than zero in fcTL chunk");
Checked<int> left { static_cast<int>(fcTL.x_offset) }; Checked<int> left { static_cast<int>(fcTL.x_offset) };
Checked<int> top { static_cast<int>(fcTL.y_offset) }; Checked<int> top { static_cast<int>(fcTL.y_offset) };
@ -1157,9 +1158,9 @@ static bool process_fcTL(ReadonlyBytes data, PNGLoadingContext& context)
auto right = left + width; auto right = left + width;
auto bottom = top + height; auto bottom = top + height;
if (left < 0 || width <= 0 || right.has_overflow() || right > context.width) if (left < 0 || width <= 0 || right.has_overflow() || right > context.width)
return false; return Error::from_string_literal("Invalid x_offset value in fcTL chunk");
if (top < 0 || height <= 0 || bottom.has_overflow() || bottom > context.height) if (top < 0 || height <= 0 || bottom.has_overflow() || bottom > context.height)
return false; return Error::from_string_literal("Invalid y_offset value in fcTL chunk");
bool is_first_animation_frame = context.animation_frames.is_empty(); bool is_first_animation_frame = context.animation_frames.is_empty();
if (!is_first_animation_frame) if (!is_first_animation_frame)
@ -1169,61 +1170,61 @@ static bool process_fcTL(ReadonlyBytes data, PNGLoadingContext& context)
if (!context.has_seen_idat_chunk && is_first_animation_frame) if (!context.has_seen_idat_chunk && is_first_animation_frame)
context.is_first_idat_part_of_animation = true; context.is_first_idat_part_of_animation = true;
return true; return {};
} }
static bool process_fdAT(ReadonlyBytes data, PNGLoadingContext& context) static ErrorOr<void> process_fdAT(ReadonlyBytes data, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#fdAT-chunk // https://www.w3.org/TR/png/#fdAT-chunk
if (data.size() <= 4) if (data.size() <= 4)
return false; return Error::from_string_literal("fdAT chunk has an abnormal size");
u32 sequence_number = *bit_cast<NetworkOrdered<u32> const*>(data.data()); u32 sequence_number = *bit_cast<NetworkOrdered<u32> const*>(data.data());
if (sequence_number != context.animation_next_expected_seq) if (sequence_number != context.animation_next_expected_seq)
return false; return Error::from_string_literal("Unexpected sequence number");
context.animation_next_expected_seq++; context.animation_next_expected_seq++;
if (context.animation_frames.is_empty()) if (context.animation_frames.is_empty())
return false; return Error::from_string_literal("No frame available");
auto& current_animation_frame = context.animation_frames[context.animation_frames.size() - 1]; auto& current_animation_frame = context.animation_frames[context.animation_frames.size() - 1];
auto compressed_data = data.slice(4); auto compressed_data = data.slice(4);
current_animation_frame.compressed_data.append(compressed_data.data(), compressed_data.size()); current_animation_frame.compressed_data.append(compressed_data.data(), compressed_data.size());
return true; return {};
} }
static bool process_IEND(ReadonlyBytes, PNGLoadingContext& context) static void process_IEND(ReadonlyBytes, PNGLoadingContext& context)
{ {
// https://www.w3.org/TR/png/#11IEND // https://www.w3.org/TR/png/#11IEND
if (context.has_seen_actl_chunk_before_idat) if (context.has_seen_actl_chunk_before_idat)
context.last_completed_animation_frame_index = context.animation_frames.size(); context.last_completed_animation_frame_index = context.animation_frames.size();
context.has_seen_iend = true; context.has_seen_iend = true;
return true;
} }
static bool process_chunk(Streamer& streamer, PNGLoadingContext& context) static ErrorOr<void> process_chunk(Streamer& streamer, PNGLoadingContext& context)
{ {
u32 chunk_size; u32 chunk_size;
if (!streamer.read(chunk_size)) { if (!streamer.read(chunk_size)) {
dbgln_if(PNG_DEBUG, "Bail at chunk_size"); dbgln_if(PNG_DEBUG, "Bail at chunk_size");
return false; return Error::from_string_literal("Error while reading from Streamer");
} }
Array<u8, 4> chunk_type_buffer; Array<u8, 4> chunk_type_buffer;
StringView const chunk_type { chunk_type_buffer.span() }; StringView const chunk_type { chunk_type_buffer.span() };
if (!streamer.read_bytes(chunk_type_buffer.data(), chunk_type_buffer.size())) { if (!streamer.read_bytes(chunk_type_buffer.data(), chunk_type_buffer.size())) {
dbgln_if(PNG_DEBUG, "Bail at chunk_type"); dbgln_if(PNG_DEBUG, "Bail at chunk_type");
return false; return Error::from_string_literal("Error while reading from Streamer");
} }
ReadonlyBytes chunk_data; ReadonlyBytes chunk_data;
if (!streamer.wrap_bytes(chunk_data, chunk_size)) { if (!streamer.wrap_bytes(chunk_data, chunk_size)) {
dbgln_if(PNG_DEBUG, "Bail at chunk_data"); dbgln_if(PNG_DEBUG, "Bail at chunk_data");
return false; return Error::from_string_literal("Error while reading from Streamer");
} }
u32 chunk_crc; u32 chunk_crc;
if (!streamer.read(chunk_crc)) { if (!streamer.read(chunk_crc)) {
dbgln_if(PNG_DEBUG, "Bail at chunk_crc"); dbgln_if(PNG_DEBUG, "Bail at chunk_crc");
return false; return Error::from_string_literal("Error while reading from Streamer");
} }
dbgln_if(PNG_DEBUG, "Chunk type: '{}', size: {}, crc: {:x}", chunk_type, chunk_size, chunk_crc); dbgln_if(PNG_DEBUG, "Chunk type: '{}', size: {}, crc: {:x}", chunk_type, chunk_size, chunk_crc);
@ -1252,8 +1253,8 @@ static bool process_chunk(Streamer& streamer, PNGLoadingContext& context)
if (chunk_type == "fdAT"sv) if (chunk_type == "fdAT"sv)
return process_fdAT(chunk_data, context); return process_fdAT(chunk_data, context);
if (chunk_type == "IEND"sv) if (chunk_type == "IEND"sv)
return process_IEND(chunk_data, context); process_IEND(chunk_data, context);
return true; return {};
} }
PNGImageDecoderPlugin::PNGImageDecoderPlugin(u8 const* data, size_t size) PNGImageDecoderPlugin::PNGImageDecoderPlugin(u8 const* data, size_t size)