diff --git a/src/libOpenImageIO/exif.cpp b/src/libOpenImageIO/exif.cpp index 9f6fc73c6a..5f61a81cba 100644 --- a/src/libOpenImageIO/exif.cpp +++ b/src/libOpenImageIO/exif.cpp @@ -161,6 +161,8 @@ tiff_data_size(TIFFDataType tifftype) static size_t sizes[] = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4 }; const int num_data_sizes = sizeof(sizes) / sizeof(*sizes); int dir_index = bitcast(tifftype); + if (dir_index == EXIF_UTF8_TYPE) + return 1; // Exif 3.0 UTF-8 string: one byte per element if (dir_index < 0 || dir_index >= num_data_sizes) { // Inform caller about corrupted entry. return -1; @@ -216,6 +218,8 @@ tiff_datatype_to_typedesc(TIFFDataType tifftype, size_t tiffcount) #endif default: break; } + if (static_cast(tifftype) == EXIF_UTF8_TYPE) + return TypeString; return TypeUnknown; } @@ -224,7 +228,13 @@ tiff_datatype_to_typedesc(TIFFDataType tifftype, size_t tiffcount) cspan tiff_dir_data(const TIFFDirEntry& td, cspan data) { - size_t len = tiff_data_size(td); + size_t elemsize = tiff_data_size(TIFFDataType(td.tdir_type)); + if (elemsize == 0 || elemsize == size_t(-1)) { + // Unrecognized type: we can't know how much data it has, and the + // size_t(-1) sentinel would wrap the bounds check below. + return cspan(); + } + size_t len = elemsize * size_t(td.tdir_count); if (len <= 4) { // Short data are stored in the offset field itself return cspan((const uint8_t*)&td.tdir_offset, len); @@ -365,19 +375,21 @@ version4uint8_handler(const TagInfo& taginfo, const TIFFDirEntry& dir, } +// The MakerNote is the one tag whose decoding re-enters the IFD walk, so it +// is the one that needs to know the recursion depth. TagInfo::HandlerFunc +// can't carry that, so the real work lives here and read_exif_tag calls this +// directly rather than through the handler pointer. static void -makernote_handler(const TagInfo& /*taginfo*/, const TIFFDirEntry& dir, - cspan buf, ImageSpec& spec, bool swapendian = false, - int offset_adjustment = 0) +decode_makernote(const TIFFDirEntry& dir, cspan buf, ImageSpec& spec, + bool swapendian, int offset_adjustment, int depth) { if (tiff_data_size(dir) <= 4) return; // sanity check if (spec.get_string_attribute("Make") == "Canon") { - std::vector ifdoffsets { 0 }; std::set offsets_seen; decode_ifd(buf, dir.tdir_offset, spec, pvt::canon_maker_tagmap_ref(), - offsets_seen, swapendian, offset_adjustment); + offsets_seen, swapendian, offset_adjustment, depth); } else { // Maybe we just haven't parsed the Maker metadata yet? // Allow a second try later by just stashing the maker note offset. @@ -386,6 +398,15 @@ makernote_handler(const TagInfo& /*taginfo*/, const TIFFDirEntry& dir, } +static void +makernote_handler(const TagInfo& /*taginfo*/, const TIFFDirEntry& dir, + cspan buf, ImageSpec& spec, bool swapendian = false, + int offset_adjustment = 0) +{ + decode_makernote(dir, buf, spec, swapendian, offset_adjustment, 0); +} + + static const TagInfo tiff_tag_table[] = { // clang-format off @@ -716,7 +737,10 @@ add_exif_item_to_spec(ImageSpec& spec, const char* name, = pvt::dataspan(*dirp, buf, offset_adjustment, 2 * count); if (dspan.empty()) return; - float* f = OIIO_ALLOCA(float, count); + // The count comes from the file: bounded by the blob length, but that + // is still far too much to put on the stack. + float* f; + OIIO_ALLOCATE_STACK_OR_HEAP(f, float, count); for (size_t i = 0; i < count; ++i) { // Because the values in the blob aren't 32-bit-aligned, memcpy // them into ints to do the swapping. @@ -742,7 +766,8 @@ add_exif_item_to_spec(ImageSpec& spec, const char* name, = pvt::dataspan(*dirp, buf, offset_adjustment, 2 * count); if (dspan.empty()) return; - float* f = OIIO_ALLOCA(float, count); + float* f; + OIIO_ALLOCATE_STACK_OR_HEAP(f, float, count); for (size_t i = 0; i < count; ++i) { // Because the values in the blob aren't 32-bit-aligned, memcpy // them into ints to do the swapping. @@ -816,14 +841,25 @@ add_exif_item_to_spec(ImageSpec& spec, const char* name, /// integer and float data embedded in buf needs to be byte-swapped. /// Note that *dirp has not been swapped, and so is still in the native /// endianness of the file. +// An IFD may point at another IFD, and a hostile blob can chain them as deep +// as its own length allows -- each level costs a stack frame, so a few hundred +// KB of Exif is enough to exhaust the stack. Real files nest two or three +// deep. +static constexpr int max_ifd_depth = 32; + + static void read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, bool swab, int offset_adjustment, - std::set& ifd_offsets_seen, const TagMap& tagmap) + std::set& ifd_offsets_seen, const TagMap& tagmap, + int depth) { + if (depth > max_ifd_depth) + return; + if ((const uint8_t*)dirp < buf.data() || (const uint8_t*)dirp + sizeof(TIFFDirEntry) - >= buf.data() + buf.size()) { + > buf.data() + buf.size()) { #if DEBUG_EXIF_READ std::cerr << "Ignoring directory outside of the buffer.\n"; #endif @@ -859,7 +895,7 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, auto offset = unswapped_tdir_offset; // int stored in offset itself if (swab) swap_endian(&offset); - if (offset >= size_t(buf.size())) { + if (size_t(offset) + sizeof(unsigned short) > size_t(buf.size())) { #if DEBUG_EXIF_READ unsigned int off2 = offset; swap_endian(&off2); @@ -905,7 +941,8 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, read_exif_tag( spec, (const TIFFDirEntry*)(ifd + 2 + d * sizeof(TIFFDirEntry)), buf, swab, offset_adjustment, ifd_offsets_seen, - dir.tdir_tag == TIFFTAG_EXIFIFD ? exif_tagmap : gps_tagmap); + dir.tdir_tag == TIFFTAG_EXIFIFD ? exif_tagmap : gps_tagmap, + depth + 1); #if DEBUG_EXIF_READ std::cerr << "> End EXIF\n"; #endif @@ -915,7 +952,7 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, auto offset = unswapped_tdir_offset; // int stored in offset itself if (swab) swap_endian(&offset); - if (offset >= size_t(buf.size())) { + if (size_t(offset) + sizeof(unsigned short) > size_t(buf.size())) { #if DEBUG_EXIF_READ unsigned int off2 = offset; swap_endian(&off2); @@ -933,7 +970,8 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, std::cerr << "Now we've seen offset " << offset << "\n"; #endif const unsigned char* ifd = ((const unsigned char*)buf.data() + offset); - unsigned short ndirs = *(const unsigned short*)ifd; + unsigned short ndirs; + memcpy(&ndirs, ifd, sizeof(ndirs)); // hoop jumping for ubsan if (swab) swap_endian(&ndirs); #if DEBUG_EXIF_READ @@ -943,9 +981,11 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, << "\n"; #endif for (int d = 0; d < ndirs; ++d) - read_exif_tag( - spec, (const TIFFDirEntry*)(ifd + 2 + d * sizeof(TIFFDirEntry)), - buf, swab, offset_adjustment, ifd_offsets_seen, exif_tagmap); + read_exif_tag(spec, + (const TIFFDirEntry*)(ifd + 2 + + d * sizeof(TIFFDirEntry)), + buf, swab, offset_adjustment, ifd_offsets_seen, + exif_tagmap, depth + 1); #if DEBUG_EXIF_READ std::cerr << "> End Interoperability\n\n"; #endif @@ -953,7 +993,10 @@ read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan buf, // Everything else -- use our table to handle the general case const TagInfo* taginfo = tagmap.find(dir.tdir_tag); if (taginfo && !spec.extra_attribs.contains(taginfo->name)) { - if (taginfo->handler) + if (taginfo->handler == makernote_handler) + decode_makernote(dir, buf, spec, swab, offset_adjustment, + depth + 1); + else if (taginfo->handler) taginfo->handler(*taginfo, dir, buf, spec, swab, offset_adjustment); else if (taginfo->tifftype != TIFF_NOTYPE) @@ -1094,7 +1137,7 @@ encode_exif_entry(const ParamValue& p, int tag, std::vector& dirs, bool pvt::decode_ifd(cspan buf, size_t ifd_offset, ImageSpec& spec, const TagMap& tag_map, std::set& ifd_offsets_seen, - bool swab, int offset_adjustment) + bool swab, int offset_adjustment, int depth) { // Read the directory that the header pointed to. It should contain // some number of directory entries containing tags to process. @@ -1112,7 +1155,8 @@ pvt::decode_ifd(cspan buf, size_t ifd_offset, ImageSpec& spec, for (int d = 0; d < ndirs; ++d) read_exif_tag(spec, (const TIFFDirEntry*)(ifd + 2 + d * sizeof(TIFFDirEntry)), - buf, swab, offset_adjustment, ifd_offsets_seen, tag_map); + buf, swab, offset_adjustment, ifd_offsets_seen, tag_map, + depth); return true; } @@ -1235,7 +1279,8 @@ decode_exif(cspan exif, ImageSpec& spec) // itself is also helpful in this area. if (exif.size() < sizeof(TIFFHeader)) return false; - TIFFHeader head = *(const TIFFHeader*)exif.data(); + TIFFHeader head; + memcpy(&head, exif.data(), sizeof(head)); // may be unaligned if (head.tiff_magic != 0x4949 && head.tiff_magic != 0x4d4d) return false; bool host_little = littleendian(); diff --git a/src/libOpenImageIO/exif.h b/src/libOpenImageIO/exif.h index 2789fa5b59..361cef5c27 100644 --- a/src/libOpenImageIO/exif.h +++ b/src/libOpenImageIO/exif.h @@ -31,15 +31,32 @@ namespace pvt { +// Byte length of a directory entry's data, or 0 if we can't know it. +// tiff_data_size() reports an unrecognized type as size_t(-1), which must +// never reach the bounds arithmetic below: multiplied by the count it wraps, +// and a wrapped length passes any `offset + len > size` test. +inline size_t +dirdata_size(const TIFFDirEntry& td) +{ + size_t elemsize = tiff_data_size(TIFFDataType(td.tdir_type)); + if (elemsize == 0 || elemsize == size_t(-1)) + return 0; + return elemsize * size_t(td.tdir_count); +} + + + inline const void* dataptr(const TIFFDirEntry& td, cspan data, int offset_adjustment) { - size_t len = tiff_data_size(td); + size_t len = dirdata_size(td); + if (len == 0) + return nullptr; // unknown type or no data if (len <= 4) return (const char*)&td.tdir_offset; else { - int offset = td.tdir_offset + offset_adjustment; - if (offset < 0 || size_t(offset) + len > std::size(data)) + int64_t offset = int64_t(td.tdir_offset) + offset_adjustment; + if (offset < 0 || uint64_t(offset) + len > uint64_t(std::size(data))) return nullptr; // out of bounds! return (const char*)data.data() + offset; } @@ -57,13 +74,15 @@ inline cspan dataspan(const TIFFDirEntry& td, cspan data, int offset_adjustment, size_t count) { - size_t len = tiff_data_size(td); + size_t len = dirdata_size(td); OIIO_DASSERT(len == sizeof(T) * count); + if (len == 0) + return {}; // unknown type or no data if (len <= 4) return { (const uint8_t*)&td.tdir_offset, span_size_t(len) }; else { - int offset = td.tdir_offset + offset_adjustment; - if (offset < 0 || size_t(offset) + len > std::size(data)) + int64_t offset = int64_t(td.tdir_offset) + offset_adjustment; + if (offset < 0 || uint64_t(offset) + len > uint64_t(std::size(data))) return {}; // out of bounds! return empty span return { data.data() + offset, span_size_t(len) }; } @@ -140,10 +159,12 @@ void append_tiff_dir_entry (std::vector &dirs, size_t offset_override = 0, OIIO::endian endianreq = OIIO::endian::native); +// Decode one IFD and everything it points at. `depth` is the IFD nesting +// level, incremented on each recursive step and used to bound the recursion. bool decode_ifd (cspan buf, size_t ifd_offset, ImageSpec &spec, const TagMap& tag_map, std::set& ifd_offsets_seen, bool swab=false, - int offset_adjustment=0); + int offset_adjustment=0, int depth=0); void encode_canon_makernote (std::vector& exifblob, std::vector &exifdirs, diff --git a/testsuite/jpeg-corrupt/ref/out-alt2.txt b/testsuite/jpeg-corrupt/ref/out-alt2.txt index 1d6ab6171c..c47374cc49 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt2.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt2.txt @@ -21,6 +21,23 @@ src/corrupt-exif-1626.jpg : 256 x 256, 3 channel, uint8 jpeg YResolution: 300 jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-recursive-ifd.jpg +src/corrupt-exif-recursive-ifd.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-utf8-type.jpg +src/corrupt-exif-utf8-type.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-deep-ifds.jpg +src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" corrupt-icc-4551.jpg DCT coefficient (lossy) or spatial difference (lossless) out of range Reading src/corrupt-icc-4552.jpg diff --git a/testsuite/jpeg-corrupt/ref/out-alt3.txt b/testsuite/jpeg-corrupt/ref/out-alt3.txt index 56d98f14c4..a6645903c0 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt3.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt3.txt @@ -21,6 +21,23 @@ src/corrupt-exif-1626.jpg : 256 x 256, 3 channel, uint8 jpeg YResolution: 300 jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-recursive-ifd.jpg +src/corrupt-exif-recursive-ifd.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-utf8-type.jpg +src/corrupt-exif-utf8-type.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-deep-ifds.jpg +src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" corrupt-icc-4551.jpg Reading src/corrupt-icc-4552.jpg src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg diff --git a/testsuite/jpeg-corrupt/ref/out-alt4.txt b/testsuite/jpeg-corrupt/ref/out-alt4.txt index d34a0e8b91..fc3d44696c 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt4.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt4.txt @@ -21,6 +21,23 @@ src/corrupt-exif-1626.jpg : 256 x 256, 3 channel, uint8 jpeg YResolution: 300 jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-recursive-ifd.jpg +src/corrupt-exif-recursive-ifd.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-utf8-type.jpg +src/corrupt-exif-utf8-type.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-deep-ifds.jpg +src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" corrupt-icc-4551.jpg DCT coefficient out of range Reading src/corrupt-icc-4552.jpg diff --git a/testsuite/jpeg-corrupt/ref/out-alt5.txt b/testsuite/jpeg-corrupt/ref/out-alt5.txt index fcf324fa11..1d4230ab09 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt5.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt5.txt @@ -21,6 +21,23 @@ src/corrupt-exif-1626.jpg : 256 x 256, 3 channel, uint8 jpeg YResolution: 300 jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-recursive-ifd.jpg +src/corrupt-exif-recursive-ifd.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-utf8-type.jpg +src/corrupt-exif-utf8-type.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-deep-ifds.jpg +src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" corrupt-icc-4551.jpg iconvert ERROR copying "src/corrupt-icc-4551.jpg" to "out-4551.jpg" : JPEG error: Corrupt JPEG data: bad Huffman code ("src/corrupt-icc-4551.jpg") diff --git a/testsuite/jpeg-corrupt/ref/out.txt b/testsuite/jpeg-corrupt/ref/out.txt index 0e10462c6d..c0a35b1e6e 100644 --- a/testsuite/jpeg-corrupt/ref/out.txt +++ b/testsuite/jpeg-corrupt/ref/out.txt @@ -21,6 +21,23 @@ src/corrupt-exif-1626.jpg : 256 x 256, 3 channel, uint8 jpeg YResolution: 300 jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-recursive-ifd.jpg +src/corrupt-exif-recursive-ifd.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-utf8-type.jpg +src/corrupt-exif-utf8-type.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-exif-deep-ifds.jpg +src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" corrupt-icc-4551.jpg DCT coefficient (lossy) or spatial difference (lossless) out of range Reading src/corrupt-icc-4552.jpg diff --git a/testsuite/jpeg-corrupt/run.py b/testsuite/jpeg-corrupt/run.py index a2ae4becbe..09c0973fc4 100755 --- a/testsuite/jpeg-corrupt/run.py +++ b/testsuite/jpeg-corrupt/run.py @@ -21,6 +21,24 @@ # nonsensical length, that before being fixed, caused a buffer overrun. command += info_command ("src/corrupt-exif-1626.jpg", safematch=True) +# This file's Exif block has an ExifIFD pointer whose offset lands one byte +# before the end of the Exif buffer. The shared decoder's recursive IFD branch +# read a 2-byte directory count there; before being fixed it ran one byte past +# the buffer (heap OOB read). Must now be dropped cleanly. +command += info_command ("src/corrupt-exif-recursive-ifd.jpg", safematch=True) + +# This file's Exif block declares a tag with TIFF data type 129, the Exif 3.0 +# UTF-8 type. tiff_data_size() didn't know that type and reported size_t(-1), +# which wrapped the shared decoder's bounds arithmetic into a span of length +# size_t(-1): a heap OOB read under a sanitizer, and an uncaught +# std::length_error that aborted the process on a release build. +command += info_command ("src/corrupt-exif-utf8-type.jpg", safematch=True) + +# This file's Exif block chains 100 ExifIFD pointers, each aimed at the next. +# Every level costs a stack frame, so a large enough chain exhausts the stack; +# the decoder now stops descending after a fixed depth. +command += info_command ("src/corrupt-exif-deep-ifds.jpg", safematch=True) + # This file has a corrupted ICC profile block that has tags that say they # extend beyond the boundaries of the ICC block itself. command += run_app (oiiotool("--echo corrupt-icc-4551.jpg")) diff --git a/testsuite/jpeg-corrupt/src/corrupt-exif-deep-ifds.jpg b/testsuite/jpeg-corrupt/src/corrupt-exif-deep-ifds.jpg new file mode 100644 index 0000000000..9b9caedfd2 Binary files /dev/null and b/testsuite/jpeg-corrupt/src/corrupt-exif-deep-ifds.jpg differ diff --git a/testsuite/jpeg-corrupt/src/corrupt-exif-recursive-ifd.jpg b/testsuite/jpeg-corrupt/src/corrupt-exif-recursive-ifd.jpg new file mode 100644 index 0000000000..47092bd4aa Binary files /dev/null and b/testsuite/jpeg-corrupt/src/corrupt-exif-recursive-ifd.jpg differ diff --git a/testsuite/jpeg-corrupt/src/corrupt-exif-utf8-type.jpg b/testsuite/jpeg-corrupt/src/corrupt-exif-utf8-type.jpg new file mode 100644 index 0000000000..9d240736a1 Binary files /dev/null and b/testsuite/jpeg-corrupt/src/corrupt-exif-utf8-type.jpg differ diff --git a/testsuite/jpeg-corrupt/src/make-exif-fixtures.py b/testsuite/jpeg-corrupt/src/make-exif-fixtures.py new file mode 100644 index 0000000000..27cfc7e8e7 --- /dev/null +++ b/testsuite/jpeg-corrupt/src/make-exif-fixtures.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +"""Generator for the malformed-Exif .jpg fixtures in this directory. + +The files it writes are committed, so this only needs to be run if they must +be regenerated: + + python3 make-exif-fixtures.py . + +Each fixture is a valid 1x1 JPEG with a hand-built APP1 Exif payload prepended, +aimed at the shared Exif decoder rather than at libjpeg: + + corrupt-exif-utf8-type.jpg A tag whose TIFF data type is 129, the Exif 3.0 + UTF-8 type. tiff_data_size() knew nothing about + it and reported size_t(-1), which wrapped the + decoder's bounds arithmetic into a span of + length size_t(-1) (heap OOB read). + corrupt-exif-deep-ifds.jpg A chain of ExifIFD pointers, each aimed at the + next. Every level costs a stack frame; the + decoder now stops descending well before this. +""" + +import struct +import sys +import os + +# A valid 1x1 grayscale JPEG written by oiiotool, with its APP1 Exif +# segment stripped, embedded here so the fixtures don't depend on a JPEG +# writer and carry no version-dependent metadata of their own. +BASE_JPEG = bytes.fromhex( + 'ffd8ffe000104a46494600010100000100010000ffdb004300010101010101010101' + '01010101010102010101010102010101020202020202020202030304030303030302' + '020304030304040404040203050504040504040404ffc0000b080001000101011100' + 'ffc4001f0000010501010101010100000000000000000102030405060708090a0bff' + 'c400b5100002010303020403050504040000017d0102030004110512213141061351' + '6107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728' + '292a3435363738393a434445464748494a535455565758595a636465666768696a73' + '7475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2' + 'b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8' + 'e9eaf1f2f3f4f5f6f7f8f9faffda0008010100003f00ff003ffaffd9') + + +def app1(exif_payload): + body = b'Exif\0\0' + exif_payload + return b'\xff\xe1' + struct.pack('>H', len(body) + 2) + body + + +def tiff_header(diroff=8): + return b'II' + struct.pack(' 1 else '.') diff --git a/testsuite/png-damaged/ref/out.txt b/testsuite/png-damaged/ref/out.txt index 4998469ba9..d2e8c98972 100644 --- a/testsuite/png-damaged/ref/out.txt +++ b/testsuite/png-damaged/ref/out.txt @@ -11,3 +11,14 @@ idiff ERROR: Could not read invalid_gray_alpha_sbit.png: PNG read error: oFFs: Read error: hit end of file in png reader Invalid image file "invalid_gray_alpha_sbit.png": Read error: hit end of file in png reader PNG read error: oFFs: Read error: hit end of file in png reader +Reading src/exif-utf8-type.png +src/exif-utf8-type.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + ImageDescription: "" + oiio:ColorSpace: "srgb_rec709_scene" +Reading src/exif-deep-ifds.png +src/exif-deep-ifds.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" diff --git a/testsuite/png-damaged/run.py b/testsuite/png-damaged/run.py index 4db5c18586..1d003886a0 100755 --- a/testsuite/png-damaged/run.py +++ b/testsuite/png-damaged/run.py @@ -12,3 +12,10 @@ command += rw_command (OIIO_TESTSUITE_IMAGEDIR + "/broken", "invalid_gray_alpha_sbit.png", printinfo=False) + +# These carry malformed Exif payloads in a PNG eXIf chunk, reaching the same +# shared decoder as the .jpg fixtures in testsuite/jpeg-corrupt/src. The eXIf +# length field is 31 bits, so this is the route where a deeply nested IFD +# chain can be made large enough to exhaust the stack. +command += info_command ("src/exif-utf8-type.png", safematch=True) +command += info_command ("src/exif-deep-ifds.png", safematch=True) diff --git a/testsuite/png-damaged/src/exif-deep-ifds.png b/testsuite/png-damaged/src/exif-deep-ifds.png new file mode 100644 index 0000000000..86b860869a Binary files /dev/null and b/testsuite/png-damaged/src/exif-deep-ifds.png differ diff --git a/testsuite/png-damaged/src/exif-utf8-type.png b/testsuite/png-damaged/src/exif-utf8-type.png new file mode 100644 index 0000000000..63ea003c2a Binary files /dev/null and b/testsuite/png-damaged/src/exif-utf8-type.png differ diff --git a/testsuite/png-damaged/src/make-exif-fixtures.py b/testsuite/png-damaged/src/make-exif-fixtures.py new file mode 100644 index 0000000000..0afe69e980 --- /dev/null +++ b/testsuite/png-damaged/src/make-exif-fixtures.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +"""Generator for the malformed-Exif .png fixtures in this directory. + +The files it writes are committed, so this only needs to be run if they must +be regenerated: + + python3 make-exif-fixtures.py . + +These carry the same hand-built Exif payloads as the .jpg fixtures in +testsuite/jpeg-corrupt/src, but through PNG's eXIf chunk instead of JPEG's +APP1 marker. The two containers reach the same shared decoder, and the PNG +route matters on its own because an eXIf chunk's length field is 31 bits -- +a JPEG APP1 marker caps out at 64 KB, so PNG is where a deeply nested IFD +chain can be made large enough to exhaust the stack. + + exif-utf8-type.png A tag whose TIFF data type is 129, the Exif 3.0 UTF-8 + type, which tiff_data_size() reported as size_t(-1). + exif-deep-ifds.png A chain of ExifIFD pointers, each aimed at the next. +""" + +import binascii +import os +import struct +import sys +import zlib + + +def chunk(kind, data): + return (struct.pack('>I', len(data)) + kind + data + + struct.pack('>I', binascii.crc32(kind + data) & 0xffffffff)) + + +def png(exif_payload): + # 1x1 8-bit grayscale, one zlib-compressed scanline (filter byte + pixel). + ihdr = struct.pack('>IIBBBBB', 1, 1, 8, 0, 0, 0, 0) + idat = zlib.compress(b'\0\0') + return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', ihdr) + + chunk(b'eXIf', exif_payload) + chunk(b'IDAT', idat) + + chunk(b'IEND', b'')) + + +def tiff_header(diroff=8): + return b'II' + struct.pack(' 1 else '.')