From 34ada0cd9dcd0ecb95b741c497db22c6d2b700b7 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sun, 26 Jul 2026 11:19:54 -0700 Subject: [PATCH] fix(xmp): bound decode recursion and growth, escape encoder output The shared XMP decoder is fed file-controlled bytes from jpeg, png, tiff, psd, and heif. Three issues fixed: - Recursion depth was tracked but never enforced, so a deeply nested XMP packet (easy to make tiny via a compressed png chunk) could crash the decoder with a stack overflow. Depth is now capped at 64 levels. - Building up a spec's attribute list was quadratic: each new list item re-read and re-wrote the entire accumulated value. A crafted file with a large attribute list could burn seconds of CPU and gigabytes of memory. Added a budget capping attribute count and total attribute payload size per spec. - The XMP encoder wrote attribute values straight into XML with no escaping, so a value read from an untrusted file could break out of its attribute and forge others, or produce malformed XML. Values are now escaped. Added fixtures covering all three cases. Known gap left as follow-up: decode_xmp() always returns true, so these caps truncate silently instead of surfacing an error. Assisted-by: Claude Code / claude-opus-5 Signed-off-by: Larry Gritz --- src/libOpenImageIO/xmp.cpp | 108 +++++++++++++++--- testsuite/jpeg-corrupt/ref/out-alt2.txt | 5 + testsuite/jpeg-corrupt/ref/out-alt3.txt | 5 + testsuite/jpeg-corrupt/ref/out-alt4.txt | 5 + testsuite/jpeg-corrupt/ref/out-alt5.txt | 5 + testsuite/jpeg-corrupt/ref/out.txt | 5 + testsuite/jpeg-corrupt/run.py | 7 ++ .../src/corrupt-xmp-deep-nesting.jpg | Bin 0 -> 65869 bytes .../jpeg-corrupt/src/make-xmp-fixtures.py | 77 +++++++++++++ testsuite/jpeg-metadata/ref/out.txt | 7 ++ testsuite/jpeg-metadata/run.py | 9 ++ testsuite/png-damaged/ref/out.txt | 11 ++ testsuite/png-damaged/run.py | 21 ++++ .../png-damaged/src/make-xmp-fixtures.py | 101 ++++++++++++++++ .../png-damaged/src/xmp-deep-nesting.png | Bin 0 -> 2558 bytes testsuite/png-damaged/src/xmp-list-blowup.png | Bin 0 -> 52871 bytes .../png-damaged/src/xmp-many-attribs.png | Bin 0 -> 39992 bytes .../png-damaged/src/xmp-multi-packet.png | Bin 0 -> 76101 bytes 18 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 testsuite/jpeg-corrupt/src/corrupt-xmp-deep-nesting.jpg create mode 100644 testsuite/jpeg-corrupt/src/make-xmp-fixtures.py create mode 100644 testsuite/png-damaged/src/make-xmp-fixtures.py create mode 100644 testsuite/png-damaged/src/xmp-deep-nesting.png create mode 100644 testsuite/png-damaged/src/xmp-list-blowup.png create mode 100644 testsuite/png-damaged/src/xmp-many-attribs.png create mode 100644 testsuite/png-damaged/src/xmp-multi-packet.png diff --git a/src/libOpenImageIO/xmp.cpp b/src/libOpenImageIO/xmp.cpp index 1cc61cdaca..6030c1aa0c 100644 --- a/src/libOpenImageIO/xmp.cpp +++ b/src/libOpenImageIO/xmp.cpp @@ -280,6 +280,40 @@ parse_rational(string_view s, int& n, int& d) +// Bounds on the work an XMP decode may do. Real XMP is a few hundred bytes +// to a few tens of KB, nested a handful of levels deep, holding tens of +// attributes. These caps sit far above anything legitimate; they exist +// because every one of those quantities is otherwise bounded only by the +// length of an attacker-supplied packet. +static constexpr int max_xmp_depth = 64; +static constexpr size_t max_xmp_attribs = 4096; +static constexpr size_t max_xmp_bytes = 1024 * 1024; + + +// Running totals for one decode. `bytes` accumulates the size add_attrib() +// reports for each attribute it writes; for a list attribute that size is the +// whole re-joined list, so this total is the quadratic cost of growing the +// list, and capping it caps that cost directly. +struct XMPbudget { + size_t bytes = 0; + size_t attribs = 0; + + bool exhausted() const + { + return bytes > max_xmp_bytes || attribs > max_xmp_attribs; + } + + // Charge one attribute write; return false once the budget is gone. + bool spend(size_t nbytes) + { + bytes += nbytes; + ++attribs; + return !exhausted(); + } +}; + + + // Utility: add an attribute to the spec with the given xml name and // value. Search for it in xmptag, and if found that will tell us what // the type is supposed to be, as well as any special handling. If not @@ -433,9 +467,15 @@ add_attrib(ImageSpec& spec, string_view xmlname, string_view xmlvalue, // Return value is the size of the resulting attribute (can be used to // catch runaway or corrupt XML). static size_t -decode_xmp_node(pugi::xml_node node, ImageSpec& spec, int level = 1, - const char* parentname = NULL, bool isList = false) +decode_xmp_node(pugi::xml_node node, ImageSpec& spec, XMPbudget& budget, + int level = 1, const char* parentname = NULL, + bool isList = false) { + // Each level of XML nesting costs a stack frame here, and a packet can + // nest as deep as its own length allows, so cap the descent. + if (level > max_xmp_depth) + return 0; + std::string mylist; // will accumulate for list items size_t totalsize = 0; for (; node; node = node.next_sibling()) { @@ -459,10 +499,12 @@ decode_xmp_node(pugi::xml_node node, ImageSpec& spec, int level = 1, totalsize += sz; // As a guard against runaway lists or corrupt XMP blocks, // don't let attribute lists grow to more than 64KB each. - if (sz > 64 * 1024) + if (!budget.spend(sz) || sz > 64 * 1024) break; } } + if (budget.exhausted()) + break; if (Strutil::iequals(node.name(), "xmpMM::History")) { // FIXME -- image history is complicated. Come back to it. continue; @@ -482,12 +524,12 @@ decode_xmp_node(pugi::xml_node node, ImageSpec& spec, int level = 1, || Strutil::iequals(node.name(), "rdf:li")) { // Various kinds of lists. Recurse, pass the parent name // down, and let the child know it's part of a list. - totalsize += decode_xmp_node(node.first_child(), spec, level + 1, - parentname, true); + totalsize += decode_xmp_node(node.first_child(), spec, budget, + level + 1, parentname, true); } else { // Not a list, but it's got children. Recurse. - totalsize += decode_xmp_node(node.first_child(), spec, level + 1, - node.name(), isList); + totalsize += decode_xmp_node(node.first_child(), spec, budget, + level + 1, node.name(), isList); } // If this node has a value but no name, it's definitely part @@ -498,16 +540,22 @@ decode_xmp_node(pugi::xml_node node, ImageSpec& spec, int level = 1, mylist += ";"; mylist += node.value(); totalsize += mylist.size(); + if (mylist.size() > max_xmp_bytes) + break; } // As a guard against runaway lists or corrupt XMP blocks, // don't let attribute lists grow to more than 64KB each. if (isList && totalsize > 64 * 1024) break; + if (budget.exhausted()) + break; } // If we have accumulated a list, turn it into an attribute if (parentname && mylist.size()) { - totalsize += add_attrib(spec, parentname, mylist, true); + size_t sz = add_attrib(spec, parentname, mylist, true); + totalsize += sz; + budget.spend(sz); } return totalsize; } @@ -565,8 +613,15 @@ decode_xmp(string_view xml, ImageSpec& spec) auto first_desc = doc.find_node([](pugi::xml_node n) { return strcmp(n.name(), "rdf:Description") == 0; }); - if (first_desc) - decode_xmp_node(first_desc, spec); + if (first_desc) { + XMPbudget budget; + // A file may hold many XMP packets (png allows any number of zTXt + // chunks), and ImageSpec::attribute() is a linear scan, so the cost is + // quadratic in the spec's total attribute count, not in any one + // packet's share. Charge what the spec already holds. + budget.attribs = spec.extra_attribs.size(); + decode_xmp_node(first_desc, spec, budget); + } #if DEBUG_XMP_READ std::cerr << "XMP total parse time " << timer() << "\n"; #endif @@ -576,6 +631,31 @@ decode_xmp(string_view xml, ImageSpec& spec) +// Escape the XML metacharacters so a value cannot close its own quote (or its +// element) and turn into markup. Values reaching the encoder are ordinary +// metadata, which may equally well have arrived from a file read by +// decode_xmp(), so they are not trusted to be XML-safe. Apostrophes are left +// alone: every value we emit is inside double quotes, so escaping them would +// only churn the output of files that are already fine. +static std::string +xml_escape(string_view s) +{ + std::string out; + out.reserve(s.size()); + for (char c : s) { + switch (c) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + case '"': out += """; break; + default: out += c; break; + } + } + return out; +} + + + // Turn one ParamValue (whose xmp info we know) into a properly // serialized xmp string. static std::string @@ -676,16 +756,18 @@ encode_xmp_category(std::vector>& list, if (Strutil::istarts_with(xmpname, pattern)) { std::string x; if (control == XMP_attribs) - x = Strutil::fmt::format("{}=\"{}\"", xmpname, val); + x = Strutil::fmt::format("{}=\"{}\"", xmpname, xml_escape(val)); else if (control == XMP_AltList || control == XMP_BagList) { std::vector vals; Strutil::split(val, vals, ";"); for (auto& val : vals) { val = Strutil::strip(val); - x += Strutil::fmt::format("{}", val); + x += Strutil::fmt::format("{}", + xml_escape(val)); } } else - x = Strutil::fmt::format("<{}>{}", xmpname, val, xmpname); + x = Strutil::fmt::format("<{}>{}", xmpname, + xml_escape(val), xmpname); if (!x.empty() && control != XMP_suppress) { if (!found) { // if (nodename && nodename[0]) { diff --git a/testsuite/jpeg-corrupt/ref/out-alt2.txt b/testsuite/jpeg-corrupt/ref/out-alt2.txt index c47374cc49..d62ebf012a 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt2.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt2.txt @@ -38,6 +38,11 @@ src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-xmp-deep-nesting.jpg +src/corrupt-xmp-deep-nesting.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 a6645903c0..978fb9f1be 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt3.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt3.txt @@ -38,6 +38,11 @@ src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-xmp-deep-nesting.jpg +src/corrupt-xmp-deep-nesting.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 fc3d44696c..64fea0f453 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt4.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt4.txt @@ -38,6 +38,11 @@ src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-xmp-deep-nesting.jpg +src/corrupt-xmp-deep-nesting.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 1d4230ab09..33a9a3a99f 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt5.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt5.txt @@ -38,6 +38,11 @@ src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-xmp-deep-nesting.jpg +src/corrupt-xmp-deep-nesting.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 c0a35b1e6e..cf5215d880 100644 --- a/testsuite/jpeg-corrupt/ref/out.txt +++ b/testsuite/jpeg-corrupt/ref/out.txt @@ -38,6 +38,11 @@ src/corrupt-exif-deep-ifds.jpg : 1 x 1, 1 channel, uint8 jpeg SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/corrupt-xmp-deep-nesting.jpg +src/corrupt-xmp-deep-nesting.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 09c0973fc4..4df8743b30 100755 --- a/testsuite/jpeg-corrupt/run.py +++ b/testsuite/jpeg-corrupt/run.py @@ -39,6 +39,13 @@ # the decoder now stops descending after a fixed depth. command += info_command ("src/corrupt-exif-deep-ifds.jpg", safematch=True) +# This file's APP1 XMP marker nests ~9000 XML elements, all a 64 KB marker +# has room for. decode_xmp_node() recursed once per level with nothing +# bounding the descent, enough to exhaust the stack of a sanitizer build or of +# a worker thread with a small stack. The decoder now stops descending after a +# fixed depth. +command += info_command ("src/corrupt-xmp-deep-nesting.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-xmp-deep-nesting.jpg b/testsuite/jpeg-corrupt/src/corrupt-xmp-deep-nesting.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd99ea8f7def3d9f8cb1428bb3eef4d183413e3f GIT binary patch literal 65869 zcmeIz$8XbM9L8}cO=wy!>~$fSjnue<6{|@E4Pi({q7(|;+{A4|o5Un8F_k#5$BjL1 z>~Uj{6Qj%9qru`c$iKudw%j8&avi zsj8tQw3gm;v&QWuSUXu^$0gt{;6dPB!0QkZ&=3vL5Dn1~4bcz{(GU&M5Dn1~4bcz{ z(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M z5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~ z4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{ z(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M z5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~ z4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{ z(GU&M5Dn1~4bcz{(GU&M5Dn1~4bcz{(GU&M5Dn4rztT`j#~S3Ynu(_4njTLjgO$0F znsCHw8M2Yk5~^~cEohRZHzf!pnJS)0pcUJ?3w>^z5~zhJftBdu_EZ={efhxJ2upF&Sj zabIh~xqgDvVWqip8P@F8)^Wkr-#uVd*^E8|7bu0|ib3UTwjJ<{o_Xr(;F^0M##Gd& z*A^8I89HqEh_U0wPnbAq@|3C5ru*fp@T|z}IdkVNtX;IYZpqSR(U{uM*c4x$(OXw$ zja6;y)^FIjY4et?+js2TwR_Lreftj{I(+2lvEwIBo<4K--1!R^FI~QNz5T|`Tet7r xz5n3hqsLD=I=i}`zj*oT^_#cv-hce``ODXD-+%n' + '' + '' + '') +FOOT = '' + + +def app1_xmp(xmp): + body = XMP_URI + xmp.encode('utf-8') + assert len(body) + 2 <= 0xffff, 'APP1 payload exceeds the marker limit' + return b'\xff\xe1' + struct.pack('>H', len(body) + 2) + body + + +def deep_nesting_xmp(): + # Fill the marker. The tags are left unclosed so a level costs 7 bytes + # rather than 15; pugixml's fragment mode builds the nesting either way. + budget = 0xffff - 2 - len(XMP_URI) - len(HEAD) - len(FOOT) + return HEAD + '' * (budget // 7) + FOOT + + +def main(dir): + path = os.path.join(dir, 'corrupt-xmp-deep-nesting.jpg') + with open(path, 'wb') as f: + f.write(BASE_JPEG[:2] + app1_xmp(deep_nesting_xmp()) + BASE_JPEG[2:]) + + +if __name__ == '__main__': + main(sys.argv[1] if len(sys.argv) > 1 else '.') diff --git a/testsuite/jpeg-metadata/ref/out.txt b/testsuite/jpeg-metadata/ref/out.txt index 86cee1f4ec..a562ef7603 100644 --- a/testsuite/jpeg-metadata/ref/out.txt +++ b/testsuite/jpeg-metadata/ref/out.txt @@ -110,3 +110,10 @@ with-colon-desc.jpg : 640 x 480, 3 channel, uint8 jpeg Exif:FlashPixVersion: "0100" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +Reading xmp-escape.jpg +xmp-escape.jpg : 1 x 1, 3 channel, uint8 jpeg + SHA-1: 29E2DCFBB16F63BB0254DF7585A15BB6FB5E927D + channel list: R, G, B + IPTC:RightsUsageTerms: "ad" + jpeg:subsampling: "4:2:0" + oiio:ColorSpace: "srgb_rec709_scene" diff --git a/testsuite/jpeg-metadata/run.py b/testsuite/jpeg-metadata/run.py index ccfdb3cf3a..655802cf2d 100755 --- a/testsuite/jpeg-metadata/run.py +++ b/testsuite/jpeg-metadata/run.py @@ -36,3 +36,12 @@ extraargs="--attrib:type=string ImageDescription \"Example:Text\"") command += info_command ("with-colon-desc.jpg", safematch=True, extraargs="--oiioattrib:type=int jpeg:com_attributes 0") + +# The XMP encoder interpolates attribute values into XML text, so a value +# holding XML metacharacters has to be escaped or it becomes markup. This one +# is carried only by XMP (not also by the IPTC IIM block); unescaped, it +# truncated to "a" and took the rest of the packet with it. +command += oiiotool ("--create 1x1 3 " + "--attrib:type=string IPTC:RightsUsageTerms \"ad\" " + "-o xmp-escape.jpg") +command += info_command ("xmp-escape.jpg", safematch=True) diff --git a/testsuite/png-damaged/ref/out.txt b/testsuite/png-damaged/ref/out.txt index d2e8c98972..b9d02160bb 100644 --- a/testsuite/png-damaged/ref/out.txt +++ b/testsuite/png-damaged/ref/out.txt @@ -22,3 +22,14 @@ src/exif-deep-ifds.png : 1 x 1, 1 channel, uint8 png SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F channel list: Y oiio:ColorSpace: "srgb_rec709_scene" +Reading src/xmp-deep-nesting.png +src/xmp-deep-nesting.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F + channel list: Y + oiio:ColorSpace: "srgb_rec709_scene" +src/xmp-list-blowup.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F +src/xmp-many-attribs.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F +src/xmp-multi-packet.png : 1 x 1, 1 channel, uint8 png + SHA-1: 5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F diff --git a/testsuite/png-damaged/run.py b/testsuite/png-damaged/run.py index 1d003886a0..09fd03a7ea 100755 --- a/testsuite/png-damaged/run.py +++ b/testsuite/png-damaged/run.py @@ -19,3 +19,24 @@ # 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) + +# These carry hostile XMP payloads in a compressed zTXt chunk, reaching the +# shared XMP decoder. PNG is the useful container: a zTXt payload is deflated +# and its length is 31 bits, so packets far larger than a 64 KB JPEG APP1 +# marker cost only tens of KB on disk. +# +# xmp-deep-nesting nests 100000 elements; decode_xmp_node() recursed once per +# level and exhausted the stack (SIGSEGV on a plain release build). +command += info_command ("src/xmp-deep-nesting.png", safematch=True) +# The next three are volume attacks, so their metadata is deliberately not +# dumped -- what regresses is time and memory, not the attribute values. +# xmp-list-blowup appends 20000 dc:subject items; each append re-split, +# re-joined and re-interned the whole accumulated list, which took 11 s and +# 3.2 GB before the budget cap. xmp-many-attribs writes 8000 attributes into +# a spec whose attribute lookup is a linear scan. xmp-multi-packet spreads +# that same attack over 8 zTXt chunks, each within its own cap: the scan is +# over the whole spec, so a budget that reset per packet left it costing 3.8 s +# here and 211 s for a 1 MB file. +command += info_command ("src/xmp-list-blowup.png", verbose=False) +command += info_command ("src/xmp-many-attribs.png", verbose=False) +command += info_command ("src/xmp-multi-packet.png", verbose=False) diff --git a/testsuite/png-damaged/src/make-xmp-fixtures.py b/testsuite/png-damaged/src/make-xmp-fixtures.py new file mode 100644 index 0000000000..da6c7773d0 --- /dev/null +++ b/testsuite/png-damaged/src/make-xmp-fixtures.py @@ -0,0 +1,101 @@ +#!/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-XMP .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-xmp-fixtures.py . + +Each fixture is a 1x1 PNG carrying hostile XMP in compressed zTXt chunks +keyed "XML:com.adobe.xmp", aimed at the shared XMP decoder rather than at +libpng. PNG is the useful container here because a zTXt chunk's length is 31 +bits, its payload is deflated, and a file may hold any number of them, so a +packet large enough to matter costs only a few KB on disk; a JPEG APP1 marker +caps out at 64 KB and one file has only so many markers. + + xmp-deep-nesting.png Nested elements, one stack frame per level in + decode_xmp_node(). Unbounded before the depth cap. + xmp-list-blowup.png Many dc:subject list items. Each one re-split, + re-joined and re-interned the whole accumulated list, + so the cost was quadratic in the item count. + xmp-many-attribs.png Many distinct attributes. ImageSpec::attribute() is a + linear scan, so this was quadratic too. + xmp-multi-packet.png The same attack spread over several packets, each + under its own cap. The scan is over the whole spec, + so the cost is quadratic in the total across + packets, not in any one packet's share. +""" + +import binascii +import os +import struct +import sys +import zlib + +XMP_KEY = b'XML:com.adobe.xmp' + +HEAD = ('' + '' + '' + '') +FOOT = '' + + +def chunk(kind, data): + return (struct.pack('>I', len(data)) + kind + data + + struct.pack('>I', binascii.crc32(kind + data) & 0xffffffff)) + + +def png(xmps): + # 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') + out = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', ihdr) + for xmp in xmps: + ztxt = XMP_KEY + b'\0\0' + zlib.compress(xmp.encode('utf-8'), 9) + out += chunk(b'zTXt', ztxt) + return out + chunk(b'IDAT', idat) + chunk(b'IEND', b'') + + +def deep_nesting_xmp(depth=100000): + return HEAD + '' * depth + '' * depth + FOOT + + +def list_blowup_xmp(items=20000): + body = ''.join('' % i for i in range(items)) + return HEAD + body + FOOT + + +def many_attribs_xmp(count=8000): + attrs = ''.join(' foo:a%d="v%d"' % (i, i) for i in range(count)) + return HEAD + '' + FOOT + + +def multi_packet_xmps(packets=8, per_packet=4200): + # Each packet is over the per-decode attribute cap on its own, and the + # names are distinct across packets so every one of them grows the spec + # instead of overwriting what the last one wrote. + names = iter(range(packets * per_packet)) + return [HEAD + '' + FOOT for p in range(packets)] + + +def main(dir): + for name, xmps in [('xmp-deep-nesting.png', [deep_nesting_xmp()]), + ('xmp-list-blowup.png', [list_blowup_xmp()]), + ('xmp-many-attribs.png', [many_attribs_xmp()]), + ('xmp-multi-packet.png', multi_packet_xmps())]: + with open(os.path.join(dir, name), 'wb') as f: + f.write(png(xmps)) + + +if __name__ == '__main__': + main(sys.argv[1] if len(sys.argv) > 1 else '.') diff --git a/testsuite/png-damaged/src/xmp-deep-nesting.png b/testsuite/png-damaged/src/xmp-deep-nesting.png new file mode 100644 index 0000000000000000000000000000000000000000..055c5ffcc0812b36c8b82ce2ca551070bcbf8b4a GIT binary patch literal 2558 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`{B`aIUWki71Ki^|4CM&(%vz$xlkv ztH>>2V5qqD_T28i%LxK(7s5CC%&ic15!9aEl;OHrgj;B9^8cG#h2m4HMJF#m-nhZk z)avR?$=Y8gukYSS`>DxuQE|_duWEgZH{Cd5Z)a<3vvvOA?4M6>wdP-6fB*Aq&3Ms| zdAsE`lJZVE65|)+YIf-yXJK>-sP5 z*1po`_ouqfYbjpgWc%f0(ms!WY5l&R<@l1rwmvsFKU-_&yY1!bmv}%SF$zXQU^E0q zSP1<0ahjy!If_O@U^E0qLtubI;D2{)q0je)=XYUkyu~QM7yG z*-ya|ivq-F{}6h=Z9l^fr*&JaY?p5V7S~*!E{-7;bCMH)Y$gUqkM&Cmfh-13S3j3^ HP6lC}iY zK#{7YmB^(a*C0YcKu}ajtpxB+ z_S$Q`ug~mgXQ!3p2{QsgknyX;%hoX?YW&M$evn1spSdAO?SfEO_dV{b-(9pRVCSrj zn*)5e%nI2Vh#(=i!}P0F;X6W=od>y*li#_Z(iM&S%Hj%Z&e^d}bbnzA9fGYW*hc?`!Hwkt8`DL{zOFo*Y<`Qd;LSDKFVHS7oiWP4R!Qz zoncUA&@1VPA1dhq4c$f?GU$6MT1nrVO83!&v+2w9P%M4V-q5R48HOwM0fy$v;j5Ly zKid!gtTYVFrhoD=JPt7QC=KmC#&z!b==Bl(jXuLaMi^9;L!1ah>u_1ba5jB^wxNdZ zp?fRozte+WM&BnQ44o1EhwX>k?CDN=IDp=*|5qhlLl1CzhlYltl*3DZs2sRu-`+ah zNDl_n!zbtmBaQ;-AthBg)M&5ook|Z*H4La|gU0YvrQxxUA*zyILl67V1WiYLTRGT4 z4_NA>=)Ty9hxCw(;pS9Yn^8H?*lM`1F$^dTru2M+He&cJ-PlU&>4CBIeILVv0J<%} z&=)}W>mr7tBJ_H?#;2!s_>of2yxp!eG)DB(hWoMfVC#da5r$z7eal{ci}T=-EL4Bm z$NuCk&f`b21A2=2qZ?p{(GAMV$03IHj)Cmh&e`@!m4*-#(>lC;;gYv))SY7n46XF} zC3YjzJJXa??(UX_`N!3b?>@g|vCSDBJuYcV({5R)zw@t9=NSE+)5TPU>1slvA$nmi-FZ*?iYIX6O!3PWfrK>>xe2?fj)*AS!9f zo!znn{(*Jld(K~4Y*V7Me?|EO(k`~?Pnr@|H@>2COzUo$=KLky9qPcXOCwXwZdq&R zn992G;mRq)b~gP7{k21PDBbx>8rzTS#TxP8erZFU?XmEdQ+*FjzEB3gp)Ys2-`VVF zuW=vTuS(t=raH8J*}T`L+&IcwnJ~Sfw^`Avs1Z-#!5gb>-|T2U*{ir7BHda(cW%q6 z$(s5jS1S)z-U=z+TE1y+OVMP_og={k)wT;dHhkXeR2yQm&D`!th^n`?HY9jk`Q^DS z_a(_pNor(QWwe6CQ=F`23`Vi^%^0{xdylSQ~JsKQcJ=-p; zYMYyelieap)bPFuKKw)3@`ueS1O7L{inmvIz11p8JQUl*Y;R=xti_eill{|Nl$$GV zUEKC*zx71r*7lBf!;+#o#nx@TFezV(HkpTzcZYh*c- znP0+u&WlCGq)=7wQyY2KVp#j?_e^G9u;C|mvn%FY3I0ekdwi?NQ^U&*KKyOj@*kU1 z9{Jx0E#7{?tFT4nc}V4KjL$fIk;Q~oE91%iV*INGyEU)l$Gu^dd9%N3eUB3C3#(Dg z{=QdaTd_6bDbwnQV~m-3;kpbgtC?*)lhHi$)})SP=1G{pdj2i?S1oGy8#D9UMxM18 z)aEo8GxPk0pV-Z|Xs$ZvrHLwT5#84CehxnTxNP}QbBf;oMtJe|O0U9J(d|PkzaNQ@ znZC$k;+n@7jG1|2{G7$h7JI^s@%^@(hPU(WhtW}Xy-k)DLruGtnt9s0mKORe=aeD+ zOtn7kSylr^&SVtNFR#Hz&SVtNQ!V1d|IPE$`!i^-_Zu_w{DzRd1-`|_2GYk30xPTdLpQJq(8!up+@k~bZZ0;>N zV9d-Yo(Hr&Eym0|zu_kl&i3ANrR<32#N<{{sfO1ae7L7Jq&zE zYHK_>~@jP_+*o2ZddhPg3 z><5awE_n5|h(a~I3&Dq5%9d-IQ*{0}*q?vLQ2eNm-&t=yQWK-REieba_xQa#slWGl z%IBTG_tPq&gTy&*-V>jta_o;U4N`qB0U1^Bw zn67lcDpgk!N>6k^^4TV>H1$BwabkwYdX9n_2KAg;W-#bECKR2v=WjnnPU*K_5J}4f z%pjv>w#@JWEqjL&sN-}DMtgRmKmH39Fu!Awa&nC zVg`wUqqs-s{EhchiDSyBJxl7{(9tPX*)WK260lewg7n27*tw~ARym;J!_{TGP#$6C-SG6BgTYkF6L78S3 zeK3A#M*pe@$GR-PUsI(_vyYC5r)MCJSOUvxva`eKw0}k)ihpS)%W*=2m6eBc)#|OAuRq>gyVnZBKm0jl$i@0gA%%|zF%9F znzk}JHgYN#Ny8#t$vEPbb?7qwX)*pvCOMr;reX1}WIW-%4tzdSlLo&`>^DgKINMJ1&)h z8C;Rm1j`kTdfLf-{(j8H z)KLI-;MTchI+rTMk|anIA@@Yr34kB&oJ(eLK@pZGLB1u_o~Wk)yoU$ol6hRJ2&<4F z-w`Y?)K>uB$K!Iz0xtLxtCb+P39%RICjdL~{9N)&F7+kWB0=sDaxXMU0Q~X#T(Xo4 zim@IE(n6@c&`<#gz_qz#1(zzu3=)JQSnJV90SLrR^T;YLD8WpmNIM~3kH!eVF5Ehg ztmaZBn1vL%OUT!w@d6NpJLi$txZpfCON#tJsMn*30qq$qYVNOiaV>wZZ5ch8kJYPjV=28_{ixe3qcJ2s^&Dw~*CIlbgarxu~9;m{c+!2B;-iY#r z;6pq=pXBkVD$Ls*`5(4?BRWS2KEmtsNi!a}jQP7GCT#UabdeB5;o5xCf=6A(!rc*5 zHp>@v5CS=FdX{{R2d-cWcVq%v?29@IK{RfCmVBK@UBQyvkx6X1FSg3$}U_8Y%>_xb`e*$D?X6gF7;f&DxAc z3PBuhT0lDRfEqLLKwe{uH={8^@G)*(K(63XYRtj|nZcHCM&pGb9(OJv9eLm@Y?cS| zI$OONO%#G-cwhnP#G}5#>^u-FHfsx-A_NI|TmiX`2d-gG9tfW;-hyTc!6$fr0V&~8 z*D!AnM97wJK?{W7I9^{sdh)>6n7;=yi>=;*mI}cMTw6eT^Qf<}a1X?q&Dx4q3BgI+ z^c?BS1J^Ny2Qr5(-ilrmf=_YlbL3VYbsbCcK;B}@x1tR~kcc~b8!XQQna@^l zMQ;niXL#T_(w|3tgH?DS3)rk}XuA-6j>nxNgLt48tMxz@vBlfaZXq~@=bt0@@u*s? z#RGYpE#HPd5P~GU{u~*~19e!B2V%!oZ$k%#K#6P5k>Nb54l{Tl|75eaqnHSs#!U;! zNFJ!iOgs?>ws<>Ah(I!KT}Xb!qv|mWPh=Tez8#$)0x7t2AsNF1H?Ucr$O^W4J8C8Z zsd!)^so+sJFgs5~%x3LCUlV~eJg$(8=Ya;y$rEv8i+7-W5%>bnFC>ris0Pg26M2^{ z-+|5%fpomSkWA!(M$F$6abl}?po>Hx1J@RkNj$0%3-?5v*(^WQK?Kg=rbT254>Vy4 zPh=fi?1wsvKqhWoM5gnoCM?Mlab?T>&~+k^g*z9KSv+tP%kxAeY_%WiDFWGeU=f+e zqi$jqo>kI!`u)cFh}yDeh82CFI69S6?HUeY@$cj)A zpc+CoglagN8Z!TPl&KzHk`7VX2emit`u(Pc6>%zVLm1}dKd~U_qbggQ_-XQu)4CdZ zfBc6&`27xZ@no~Kh9LL}9bO;6s}p#a1n<${9U{C`{gbO9{Fn(pS8rpR~`I*UJUwuZD-QvFc$!j{$kinyana4D%yg%Dd$TfKR-f|g{_8@s=IzbQd}s9rzwUJ|ClX7W zn!ZT?3qul@;>6OMO}Xj*^Y&(EdaZ8u>;CIP7V{pR$y~X5v0wLEm)OM6rl!>Nzc7@M z9`4uuU-`3UwLUY`e%_;ubVE~~;rSScF?fVO3L@UqUv-;Bf>haX?f+j%_CIl7(Yy`**kQJdKKsAJF2-R>jHC(A^ zSFbf+7&Cq4uDezW^Q#}HE(n?gL6{I&46qR(GeA~^iU8FRsv%Uv(bQ0Be${ei?^kQX zf+oQ!%mXX}*bM(!OfdXtZ_Vot(}3v;ufI>R9@@5|=dHi9^Iz(nAGdVFo=YMhyyL|9 z)=9liiuSM8zP&JpA+x(yN#<9NrG^AeS_46t5LgVb5g;@CDOu6?uGIKv;h%>IdsM0Y-E~7fZ8aGh`6j^3F$5K>(snU3 OWYuz~WqA%D@_zxXq&d_8 literal 0 HcmV?d00001 diff --git a/testsuite/png-damaged/src/xmp-many-attribs.png b/testsuite/png-damaged/src/xmp-many-attribs.png new file mode 100644 index 0000000000000000000000000000000000000000..91419acdf178216438c96b0f6315c72c282309bc GIT binary patch literal 39992 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 literal 0 HcmV?d00001 diff --git a/testsuite/png-damaged/src/xmp-multi-packet.png b/testsuite/png-damaged/src/xmp-multi-packet.png new file mode 100644 index 0000000000000000000000000000000000000000..a23d85ca85582aa64253f9774edc948e839c13aa GIT binary patch literal 76101 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