Skip to content

Commit 7841ab5

Browse files
authored
fix(jpeg): Improved safety and error reporting for jpeg and iptc (#5081)
* `decide_iptc_iim()` (semi-public) : change from taking a pointer & length to a string_view, and internal safety checks. * `jpeg_decode_iptc()` (non-public): change from taking an unlengthed pointer (!) to a string_view, and internal safety checks. * JpgInput::open(): a number of extra checks for memory bounds validity. --------- Signed-off-by: Larry Gritz <lg@larrygritz.com>
1 parent 10f4e75 commit 7841ab5

10 files changed

Lines changed: 106 additions & 63 deletions

File tree

src/include/OpenImageIO/tiffutils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ OIIO_API bool gps_tag_lookup (string_view name, int &tag,
210210
/// metadata without having to duplicate functionality within each
211211
/// plugin. Note that IIM is actually considered obsolete and is
212212
/// replaced by an XML scheme called XMP.
213+
OIIO_API bool decode_iptc_iim (string_view iptc, ImageSpec &spec);
214+
215+
// DEPRECATED(3.2) -- unsafe version
213216
OIIO_API bool decode_iptc_iim (const void *iptc, int length, ImageSpec &spec);
214217

215218
/// Find all the IPTC-amenable metadata in spec and assemble it into an

src/jpeg.imageio/jpeg_pvt.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ class JpgInput final : public ImageInput {
129129
// information and adding attributes to spec. This assumes it's in
130130
// the form of an IIM (Information Interchange Model), which is actually
131131
// considered obsolete and is replaced by an XML scheme called XMP.
132-
void jpeg_decode_iptc(const unsigned char* buf);
132+
// Return true if ok, false if there were corruptions or errors while
133+
// decoding the iptc.
134+
bool jpeg_decode_iptc(string_view buf);
133135

134136
bool read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec);
135137

src/jpeg.imageio/jpeginput.cpp

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,10 @@ my_error_exit(j_common_ptr cinfo)
6262
{
6363
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
6464
JpgInput::my_error_ptr myerr = (JpgInput::my_error_ptr)cinfo->err;
65+
OIIO_ASSERT(myerr);
6566

66-
/* Always display the message. */
67-
/* We could postpone this until after returning, if we chose. */
68-
// (*cinfo->err->output_message) (cinfo);
69-
myerr->jpginput->jpegerror(myerr, true);
67+
if (myerr->jpginput)
68+
myerr->jpginput->jpegerror(myerr, true);
7069

7170
/* Return control to the setjmp point */
7271
longjmp(myerr->setjmp_buffer, 1);
@@ -78,11 +77,10 @@ static void
7877
my_output_message(j_common_ptr cinfo)
7978
{
8079
JpgInput::my_error_ptr myerr = (JpgInput::my_error_ptr)cinfo->err;
80+
OIIO_ASSERT(myerr);
8181

82-
// Create the message
83-
char buffer[JMSG_LENGTH_MAX];
84-
(*cinfo->err->format_message)(cinfo, buffer);
85-
myerr->jpginput->jpegerror(myerr, false);
82+
if (myerr->jpginput)
83+
myerr->jpginput->jpegerror(myerr, false);
8684

8785
// This function is called only for non-fatal problems, so we don't
8886
// need to do the longjmp.
@@ -122,7 +120,7 @@ void
122120
JpgInput::jpegerror(my_error_ptr /*myerr*/, bool fatal)
123121
{
124122
// Send the error message to the ImageInput
125-
char errbuf[JMSG_LENGTH_MAX];
123+
char errbuf[JMSG_LENGTH_MAX] = "";
126124
(*m_cinfo.err->format_message)((j_common_ptr)&m_cinfo, errbuf);
127125
errorfmt("JPEG error: {} (\"{}\")", errbuf, filename());
128126

@@ -273,21 +271,25 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)
273271
m_spec.attribute(JPEG_SUBSAMPLING_ATTR, subsampling);
274272

275273
for (jpeg_saved_marker_ptr m = m_cinfo.marker_list; m; m = m->next) {
276-
if (m->marker == (JPEG_APP0 + 1)
277-
&& !strcmp((const char*)m->data, "Exif")) {
274+
if (m->marker == (JPEG_APP0 + 1) && m->data_length >= 4
275+
&& !strncmp((const char*)m->data, "Exif", 4)) {
278276
// The block starts with "Exif\0\0", so skip 6 bytes to get
279277
// to the start of the actual Exif data TIFF directory
280278
decode_exif(string_view((char*)m->data + 6, m->data_length - 6),
281279
m_spec);
282-
} else if (m->marker == (JPEG_APP0 + 1)
283-
&& !strcmp((const char*)m->data,
284-
"http://ns.adobe.com/xap/1.0/")) { //NOSONAR
280+
} else if (m->marker == (JPEG_APP0 + 1) && m->data_length >= 28
281+
&& !strncmp((const char*)m->data,
282+
"http://ns.adobe.com/xap/1.0/", 28)) { //NOSONAR
285283
std::string xml((const char*)m->data, m->data_length);
286284
decode_xmp(xml, m_spec);
287-
} else if (m->marker == (JPEG_APP0 + 13)
288-
&& !strcmp((const char*)m->data, "Photoshop 3.0"))
289-
jpeg_decode_iptc((unsigned char*)m->data);
290-
else if (m->marker == JPEG_COM) {
285+
} else if (m->marker == (JPEG_APP0 + 13) && m->data_length >= 13
286+
&& !strncmp((const char*)m->data, "Photoshop 3.0", 13)) {
287+
if (!jpeg_decode_iptc(
288+
string_view((const char*)m->data, m->data_length))) {
289+
errorfmt("Corrupted IPTC data");
290+
return false;
291+
}
292+
} else if (m->marker == JPEG_COM) {
291293
std::string data((const char*)m->data, m->data_length);
292294
// Additional string metadata can be stored in JPEG files as
293295
// comment markers in the form "key:value" or "ident:key:value".
@@ -646,17 +648,17 @@ JpgInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
646648
return false;
647649
}
648650

649-
int nscanlines = yend - ybegin;
651+
int64_t nscanlines = yend - ybegin;
650652
size_t sl_bytes = m_spec.scanline_bytes(true /*native*/);
651653
JSAMPLE** readdata = OIIO_ALLOCA(JSAMPLE*, nscanlines);
652-
for (int i = 0; i < nscanlines; ++i)
654+
for (int64_t i = 0; i < nscanlines; ++i)
653655
readdata[i] = reinterpret_cast<JSAMPLE*>(&data[i * sl_bytes]);
654656

655657
if (m_cmyk) {
656658
// If the file's data is CMYK, read into a 4-channel buffer, then
657659
// we'll have to convert.
658660
m_cmyk_buf.resize(m_spec.width * 4 * nscanlines);
659-
for (int i = 0; i < nscanlines; ++i)
661+
for (int64_t i = 0; i < nscanlines; ++i)
660662
readdata[i] = reinterpret_cast<JSAMPLE*>(m_cmyk_buf.data()
661663
+ i * m_spec.width * 4);
662664
OIIO_DASSERT(m_spec.nchannels == 3);
@@ -685,7 +687,7 @@ JpgInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
685687
m_next_scanline = yend;
686688

687689
if (m_cmyk) {
688-
for (int i = 0; i < nscanlines; ++i)
690+
for (int64_t i = 0; i < nscanlines; ++i)
689691
cmyk_to_rgb(make_cspan(readdata[i], m_spec.width * 4),
690692
span_cast<unsigned char>(data).subspan(
691693
m_spec.width * 3 * i, m_spec.width * 3));
@@ -717,34 +719,51 @@ JpgInput::close()
717719

718720

719721

720-
void
721-
JpgInput::jpeg_decode_iptc(const unsigned char* buf)
722+
bool
723+
JpgInput::jpeg_decode_iptc(string_view buf)
722724
{
723725
// APP13 blob doesn't have to be IPTC info. Look for the IPTC marker,
724726
// which is the string "Photoshop 3.0" followed by a null character.
725-
if (strcmp((const char*)buf, "Photoshop 3.0"))
726-
return;
727-
buf += strlen("Photoshop 3.0") + 1;
727+
if (!Strutil::starts_with(buf, "Photoshop 3.0"))
728+
return false;
729+
buf.remove_prefix(13);
730+
if (buf.size() < 1 || buf[0] != '\0')
731+
return false;
732+
buf.remove_prefix(1);
728733

729734
// Next are the 4 bytes "8BIM"
730-
if (strncmp((const char*)buf, "8BIM", 4))
731-
return;
732-
buf += 4;
735+
if (!Strutil::starts_with(buf, "8BIM"))
736+
return false;
737+
buf.remove_prefix(4);
733738

734739
// Next two bytes are the segment type, in big endian.
735740
// We expect 1028 to indicate IPTC data block.
736-
if (((buf[0] << 8) + buf[1]) != 1028)
737-
return;
738-
buf += 2;
741+
if (buf.size() < 2
742+
|| ((static_cast<unsigned char>(buf[0]) << 8)
743+
+ static_cast<unsigned char>(buf[1]))
744+
!= 1028)
745+
return false;
746+
buf.remove_prefix(2);
739747

740748
// Next are 4 bytes of 0 padding, just skip it.
741-
buf += 4;
749+
if (buf.size() < 4)
750+
return false;
751+
buf.remove_prefix(4);
742752

743753
// Next is 2 byte (big endian) giving the size of the segment
744-
int segmentsize = (buf[0] << 8) + buf[1];
745-
buf += 2;
754+
if (buf.size() < 2)
755+
return false;
756+
size_t segmentsize = (static_cast<unsigned char>(buf[0]) << 8)
757+
+ static_cast<unsigned char>(buf[1]);
758+
buf.remove_prefix(2);
759+
760+
// Ensure the declared segment size does not exceed the remaining buffer,
761+
// then restrict buf to exactly segmentsize bytes.
762+
if (segmentsize > buf.size())
763+
return false;
764+
buf = buf.substr(0, segmentsize);
746765

747-
decode_iptc_iim(buf, segmentsize, m_spec);
766+
return decode_iptc_iim(buf, m_spec);
748767
}
749768

750769
OIIO_PLUGIN_NAMESPACE_END

src/libOpenImageIO/iptc.cpp

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,26 @@ OIIO_NAMESPACE_3_1_BEGIN
103103
bool
104104
decode_iptc_iim(const void* iptc, int length, ImageSpec& spec)
105105
{
106-
const unsigned char* buf = (const unsigned char*)iptc;
106+
if (!iptc || length <= 0)
107+
return false;
108+
return decode_iptc_iim(string_view(reinterpret_cast<const char*>(iptc),
109+
static_cast<size_t>(length)),
110+
spec);
111+
}
112+
113+
107114

115+
bool
116+
decode_iptc_iim(string_view iptc, ImageSpec& spec)
117+
{
108118
#if DEBUG_IPTC_READ
109-
std::cerr << "IPTC dump:\n";
110-
for (int i = 0; i < std::min(length, 100); ++i) {
111-
if (buf[i] >= ' ' && buf[i] < 128)
112-
std::cerr << (char)buf[i] << ' ';
113-
std::cerr << "(" << int(buf[i]) << ") ";
119+
print(stderr, "IPTC dump (len={}):\n", iptc.size());
120+
for (size_t i = 0; i < std::min(iptc.size(), size_t(1000)); ++i) {
121+
if (iptc[i] >= ' ' && iptc[i] < 128)
122+
print(stderr, "{:c} ", iptc[i]);
123+
print(stderr, "({:d}) ", uint32_t(static_cast<unsigned char>(iptc[i])));
114124
}
115-
std::cerr << "\n";
125+
print(stderr, "\n");
116126
#endif
117127

118128
// Now there are a series of data blocks. Each one starts with 1C
@@ -121,28 +131,27 @@ decode_iptc_iim(const void* iptc, int length, ImageSpec& spec)
121131
// repeats until we've used up the whole segment buffer, or I guess
122132
// until we don't find another 1C 02 tag start.
123133
// N.B. I don't know why, but Picasa sometimes uses 1C 01 !
124-
while (length >= 5 && buf[0] == 0x1c
125-
&& (buf[1] == 0x02 || buf[1] == 0x01)) {
126-
int secondbyte = buf[1];
127-
int tagtype = buf[2];
128-
int tagsize = (buf[3] << 8) + buf[4];
129-
buf += 5;
130-
length -= 5;
131-
tagsize = std::min(tagsize, length);
134+
while (iptc.size() >= 5 && iptc[0] == 0x1c
135+
&& (iptc[1] == 0x02 || iptc[1] == 0x01)) {
136+
int secondbyte = static_cast<unsigned char>(iptc[1]);
137+
int tagtype = static_cast<unsigned char>(iptc[2]);
138+
size_t tagsize = (static_cast<unsigned char>(iptc[3]) << 8)
139+
+ static_cast<unsigned char>(iptc[4]);
140+
iptc.remove_prefix(5);
141+
tagsize = std::min(tagsize, iptc.size());
132142

133143
#if DEBUG_IPTC_READ
134-
std::cerr << "iptc tag " << tagtype << ", size=" << tagsize << ":\n";
135-
for (int i = 0; i < tagsize; ++i) {
136-
if (buf[i] >= ' ')
137-
std::cerr << (char)buf[i] << ' ';
138-
std::cerr << "(" << (int)(unsigned char)buf[i] << ") ";
144+
print(stderr, "iptc tag {}, size={}:\n", tagtype, tagsize);
145+
for (size_t i = 0; i < tagsize; ++i) {
146+
if (iptc[i] >= ' ')
147+
print(stderr, "{:c} ", iptc[i]);
148+
print(stderr, "({:d}) ", static_cast<unsigned char>(iptc[i]));
139149
}
140-
std::cerr << "\n";
150+
print(stderr, "\n");
141151
#endif
142152

143153
if (secondbyte == 0x02) {
144-
std::string s((const char*)buf, tagsize);
145-
154+
std::string s = iptc.substr(0, tagsize);
146155
for (int i = 0; iimtag[i].name; ++i) {
147156
if (tagtype == iimtag[i].tag) {
148157
if (iimtag[i].repeatable) {
@@ -169,8 +178,7 @@ decode_iptc_iim(const void* iptc, int length, ImageSpec& spec)
169178
}
170179
}
171180

172-
buf += tagsize;
173-
length -= tagsize;
181+
iptc.remove_prefix(tagsize);
174182
}
175183

176184
return true;

testsuite/jpeg-corrupt/ref/out-alt2.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg
4444
ICCProfile:rendering_intent: "Unknown"
4545
jpeg:subsampling: "4:2:0"
4646
oiio:ColorSpace: "srgb_rec709_scene"
47+
iinfo ERROR: "src/corrupt-iptc-8011.jpg" : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg")
48+
Corrupted IPTC data

testsuite/jpeg-corrupt/ref/out-alt3.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,5 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg
4343
ICCProfile:rendering_intent: "Unknown"
4444
jpeg:subsampling: "4:2:0"
4545
oiio:ColorSpace: "srgb_rec709_scene"
46+
iinfo ERROR: "src/corrupt-iptc-8011.jpg" : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg")
47+
Corrupted IPTC data

testsuite/jpeg-corrupt/ref/out-alt4.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg
4444
ICCProfile:rendering_intent: "Unknown"
4545
jpeg:subsampling: "4:2:0"
4646
oiio:ColorSpace: "srgb_rec709_scene"
47+
iinfo ERROR: "src/corrupt-iptc-8011.jpg" : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg")
48+
Corrupted IPTC data

testsuite/jpeg-corrupt/ref/out.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg
4444
ICCProfile:rendering_intent: "Unknown"
4545
jpeg:subsampling: "4:2:0"
4646
oiio:ColorSpace: "srgb_rec709_scene"
47+
iinfo ERROR: "src/corrupt-iptc-8011.jpg" : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg")
48+
Corrupted IPTC data

testsuite/jpeg-corrupt/run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@
2424

2525
# This file has a corrupted ICC profile block
2626
command += info_command ("src/corrupt-icc-4552.jpg", safematch=True)
27+
28+
# This file had corrupted IPTC data
29+
command += info_command ("-v src/corrupt-iptc-8011.jpg", info_program="iinfo")
3.09 KB
Loading

0 commit comments

Comments
 (0)