Talos Vulnerability Report

TALOS-2022-1627

OpenImageIO TIFF file string field information disclosure vulnerability

December 22, 2022
CVE Number

CVE-2022-41977

SUMMARY

An out of bounds read vulnerability exists in the way OpenImageIO version v2.3.19.0 processes string fields in TIFF image files. A specially-crafted TIFF file can lead to information disclosure. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

OpenImageIO Project OpenImageIO master-branch-9aeece7a
OpenImageIO Project OpenImageIO v2.3.19.0

PRODUCT URLS

OpenImageIO - https://github.com/OpenImageIO/oiio

CVSSv3 SCORE

5.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CWE

CWE-125 - Out-of-bounds Read

DETAILS

OpenImageIO is an image processing library with easy-to-use interfaces and a sizable number of supported image formats. Useful for conversion and processing and even image comparison, this library is utilized by 3D-processing software from AliceVision (including Meshroom), as well as Blender for reading Photoshop .psd files.

With OpenImageIO, handling of different file formats is a relatively simple matter. auto inp = ImageInput::open(filename) takes care of all the intricate details of figuring out which file format we’re dealing with and redirects the codeflow to the appropriate file handlers. For the TIFF file format, we first end up hitting the TIFFOpen function inside of the opensource LibTIFF library, which provides a parsed TIFF object to OpenImageIO inside of TIFFInput:seek_subimage:

bool
TIFFInput::seek_subimage(int subimage, int miplevel)
{
 // [...]
    if (!m_tif) {
        if (ioproxy_opened()) {
            static_assert(sizeof(thandle_t) == sizeof(void*),
                          "thandle_t must be same size as void*");
            // Strutil::print("\n\nOpening client \"{}\"\n", m_filename);
            ioseek(0);
            m_tif = TIFFClientOpen(m_filename.c_str(), "rm", ioproxy(),
                                   reader_readproc, reader_writeproc,
                                   reader_seekproc, reader_closeproc,
                                   reader_sizeproc, reader_mapproc,
                                   reader_unmapproc);
        } else {
#ifdef _WIN32
            std::wstring wfilename = Strutil::utf8_to_utf16wstring(m_filename);
            m_tif                  = TIFFOpenW(wfilename.c_str(), "rm");
#else
            m_tif = TIFFOpen(m_filename.c_str(), "rm");  // [1]
#endif
        }

We first hit the code path at [1] since we don’t yet have a m_tif object yet, which eventually gets us to the lengthy LibTIFF file parsing inside TIFFReadDirectory. For brevity’s sake, instead of going through the code we will just examine a sample hexdump to explain the file format:

$ hexdump -C min_crash
00000000  49 49 2a 00 08 00 00 00  13 00 00 01 03 00 01 00  |II*.............|  // [3]
00000010  00 00 50 00 00 00 01 01  03 00 01 00 00 00 40 00  |..P...........@.|
00000020  00 00 02 01 03 00 03 00  00 00 02 01 00 00 03 01  |................|
00000030  03 00 01 00 00 00 08 00  00 00 06 01 03 00 01 00  |................|
00000040  00 00 02 00 00 00 11 01  04 00 02 00 00 00 10 01  |................|
00000050  00 00 12 01 03 00 01 00  00 00 03 0a 00 00 00 01  |................|
00000060  03 00 01 00 00 00 20 00  00 00 17 01 04 00 02 00  |...... .........|
00000070  00 00 08 01 00 00 1c 01  03 00 01 00 00 00 01 00  |................|
00000080  00 00 1e 01 05 00 01 00  00 00 f2 00 00 00 1f 01  |................|
00000090  05 00 01 00 00 00 fa 00  00 00 31 01 02 00 9f 00  |..........1.....|
000000a0  00 00 1e 01 00 00 32 01  03 00 01 00 00 00 03 00  |......2.........|
000000b0  8d 57 c9 7a 71 7f 00 00  01 00 10 8b 9a 1e 01 05  |.W.zq...........|
000000c0  00 01 00 00 00 f2 00 00  00 1f 01 05 00 01 00 00  |................|
000000d0  81 2d 41 fc 06 d6 d4 56  be e8 7f 00 00 00 00 00  |.-A....V........|
000000e0  01 00 00 00 20 00 20 00  20 7a 71 7f 00 00 00 00  |.... . . zq.....|
000000f0  20 00 03 00 01 00 00 00  01 00 00 00 1e 01 05 00  | ...............|
00000100  01 00 05 00 01 00 00 81  2d 41 fc 06 01 00 00 81  |........-A......|
00000110  2d 41 fc 06                                       |-A..|
00000114



#define TIFF_BIGENDIAN      0x4d4d
#define TIFF_LITTLEENDIAN   0x4949
#define MDI_LITTLEENDIAN    0x5045
#define MDI_BIGENDIAN       0x4550


typedef struct {
    uint16_t tiff_magic;      /* magic number (defines byte order) */
    uint16_t tiff_version;    /* TIFF version number */
    uint32_t tiff_diroff;     /* byte offset to first directory */
} TIFFHeaderClassic; // [3]

typedef struct {
    uint16_t tiff_magic;      /* magic number (defines byte order) */
    uint16_t tiff_version;    /* TIFF version number */
    uint16_t tiff_offsetsize; /* size of offsets, should be 8 */
    uint16_t tiff_unused;     /* unused word, should be 0 */
    uint64_t tiff_diroff;     /* byte offset to first directory */
} TIFFHeaderBig;     // [4]

Since our first 2 bytes match the TIFF_LITTLEENDIAN header of 0x4949 [2], we deal with the TiffHeaderClassic[3] instead of the TIFFHeaderBig [4]. Thus, the uint32_t at offset 0x4 is our tiff_diroff, the offset at which we find our tiff directories (0x8). TIFFReadDirectory then goes to that offset and reads in the number of directories we have. In this case, at offset 0x8 at [2], we can see that our uint16_t dircount is 0x13. Since we are not dealing with a TIFFHeaderBig, each of these “directories” is 0xC bytes long and looks as such:

[-.-]> ptype TIFFDirEntry
type = struct TIFFDirEntry {
    uint16_t tdir_tag;
    uint16_t tdir_type;
    uint32_t tdir_count;
    uint32_t tdir_offset;
}

As such, TiffReadDirectory reads in the dircount16 * dirsize bytes immediately following our dircount. In our case, this would be 0x13 * 0xc, resulting in 0xe4 bytes being read. In the hexdump above, this would mean that our directories come directly from the bytes at offset (0xA, 0xEE). For example, if we look at the directories at offset 0x8E get parsed, we see the following:

0000009A                                 31 01 02 00 9f 00  |..........1.....|
000000a0  00 00 1e 01 00 00 32 01  03 00 01 00 00 00 03 00  |......2.........|
000000b0  8d 57 

[^~^]> p/x *direntry
$45 = {tdir_tag = 0x131, tdir_type = 0x2, tdir_count = 0x9f, tdir_offset = {toff_short = 0x132, toff_long = 0x132, toff_long8 = 0x132}, tdir_ignore = 0x0}

[^.^]> p/x *(direntry+1)
$46 = {tdir_tag = 0x132, tdir_type = 0x3, tdir_count = 0x1, tdir_offset = {toff_short = 0x578d0003, toff_long = 0x578d0003, toff_long8 = 0x578d0003}, tdir_ignore = 0x0}

The tdir_tag field is used to sort the array of directories, along with identifying the data. The tdir_type field designates the data’s type; the tdir_count designates the length of the data; and finally the tdir_offset field points to the offset inside the TIFF file where we can actually find the data. So when LibOpenImageIO wants to find the data for a given tag, it first consults the hardcoded LibTiff array of possible field types and then binary searches through the directories that it has loaded in from the file. A sample of predefined LibTiff fields is given below:

static const TIFFField
tiffFields[] = {
    { TIFFTAG_SUBFILETYPE, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_SUBFILETYPE, 1, 0, "SubfileType", NULL },
    { TIFFTAG_OSUBFILETYPE, 1, 1, TIFF_SHORT, 0, TIFF_SETGET_UNDEFINED, TIFF_SETGET_UNDEFINED, FIELD_SUBFILETYPE, 1, 0, "OldSubfileType", NULL },
    { TIFFTAG_IMAGEWIDTH, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_IMAGEDIMENSIONS, 0, 0, "ImageWidth", NULL },
    { TIFFTAG_IMAGELENGTH, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_IMAGEDIMENSIONS, 1, 0, "ImageLength", NULL },
    // [...]
}    

Which corresponds to the structure given below:

struct _TIFFField {
    uint32_t field_tag;                       /* field's tag */
    short field_readcount;                  /* read count/TIFF_VARIABLE/TIFF_SPP */
    short field_writecount;                 /* write count/TIFF_VARIABLE */
    TIFFDataType field_type;                /* type of associated data */
    uint32_t reserved;                        /* reserved for future extension */
    TIFFSetGetFieldType set_field_type;     /* type to be passed to TIFFSetField */
    TIFFSetGetFieldType get_field_type;     /* type to be passed to TIFFGetField */
    unsigned short field_bit;               /* bit in fieldsset bit vector */
    unsigned char field_oktochange;         /* if true, can change while writing */
    unsigned char field_passcount;          /* if true, pass dir count on set */
    char* field_name;                       /* ASCII name */
    TIFFFieldArray* field_subfields;        /* if field points to child ifds, child ifd field definition array */
};

For the purposes of this vulnerability, we only really care about a subset of these pre-defined TIFFFields, those with a field_readcount that’s not TIFF_VARIABLE or TIFF_SPP or TIFF_VARIABLE2, i.e. a fixed size length, and also with TIFF_SETGET_ASCII defined. These conditions lead us only to one possibility:

libtiff/tif_dirinfo.c:  { TIFFTAG_DATETIME, 20, 20, TIFF_ASCII, 0, TIFF_SETGET_ASCII, TIFF_SETGET_UNDEFINED, FIELD_CUSTOM, 1, 0, "DateTime", NULL },                                // [5]

Continuing on, let us utilize the TIFFTAG_DATETIME [5] field as our primary example. When reading this field from the file, we can see it actually being populated as such:

[~.~]> p/x *dp
$34 = {tdir_tag = 0x132, tdir_type = 0x3, tdir_count = 0x1, tdir_offset = {toff_short = 0x3, toff_long = 0x578d0003, toff_long8 = 0x578d0003}, tdir_ignore = 0x0}

There weirdly does not seem to be much correlation between the tdir_type and tdir_count (both read from the file) and the perscribed field_readcount that is given by the LibTiff definitions. The main time these facets interact is during TIFFFetchNormalTag (libtiff/tif_dirread.c):

 case TIFF_SETGET_ASCII:
            {
                uint8_t* data;
                assert(fip->field_passcount==0);
                err=TIFFReadDirEntryByteArray(tif,dp,&data);  // [6]

For each given TIFF_SETGET_* type that is defined by LibTiff, a different set of routines are called to actually read the data. For the TIFFTAG_DATETIME tag, we hit the TIFF_SETGET_ASCII case and go into TIFFReadDirEntryByteArray[6]. Based on the tdir_type field read from the file, different sized variables are read in and an appropriately sized allocation is eventually made in TIFFReadDirEntryArrayWithLimit:

static enum TIFFReadDirEntryErr TIFFReadDirEntryArrayWithLimit(
        TIFF* tif, TIFFDirEntry* direntry, uint32_t* count, uint32_t desttypesize,
        void** value, uint64_t maxcount)
{
    int typesize;
    uint32_t datasize;
    void* data;
        uint64_t target_count64;
        int original_datasize_clamped;
    typesize=TIFFDataWidth(direntry->tdir_type);

        target_count64 = (direntry->tdir_count > maxcount) ?
                maxcount : direntry->tdir_count;

    if ((target_count64==0)||(typesize==0))
    {
        *value=0;
        return(TIFFReadDirEntryErrOk);
    }
        (void) desttypesize;

        /* We just want to know if the original tag size is more than 4 bytes
         * (classic TIFF) or 8 bytes (BigTIFF)
         */
        original_datasize_clamped =
            ((direntry->tdir_count > 10) ? 10 : (int)direntry->tdir_count) * typesize;

        /*
         * As a sanity check, make sure we have no more than a 2GB tag array
         * in either the current data type or the dest data type.  This also
         * avoids problems with overflow of tmsize_t on 32bit systems.
         */
    if ((uint64_t)(2147483647 / typesize) < target_count64)
        return(TIFFReadDirEntryErrSizesan);
    if ((uint64_t)(2147483647 / desttypesize) < target_count64)
        return(TIFFReadDirEntryErrSizesan);

*count=(uint32_t)target_count64;
datasize=(*count)*typesize;
assert((tmsize_t)datasize>0);

if( isMapped(tif) && datasize > (uint64_t)tif->tif_size )
    return TIFFReadDirEntryErrIo;

if( !isMapped(tif) &&
    (((tif->tif_flags&TIFF_BIGTIFF) && datasize > 8) ||
    (!(tif->tif_flags&TIFF_BIGTIFF) && datasize > 4)) )
{
    data = NULL;
}
else
{
    data=_TIFFCheckMalloc(tif, *count, typesize, "ReadDirEntryArray");
    if (data==0)
        return(TIFFReadDirEntryErrAlloc);
}

The only real thing worth noting from the above snippet is that the size of the resultant allocation is dependant completely on the tdir_type and tdir_count fields that are read in from our tiff directory. Continuing back up toTIFFFetchNormalTag:

 case TIFF_SETGET_ASCII:
            {
                uint8_t* data;
                assert(fip->field_passcount==0);
                err=TIFFReadDirEntryByteArray(tif,dp,&data);  
               
                           
                 if (err==TIFFReadDirEntryErrOk)
                {
                    uint32_t mb = 0;
                    int n;
                    if (data != NULL)
                    {
                        uint8_t* ma = data;
                        while (mb<(uint32_t)dp->tdir_count)   // [7]
                        {
                                if (*ma==0)
                                        break;
                                ma++;
                                mb++;
                        }
                    }
                    
                    
                    if (mb+1<(uint32_t)dp->tdir_count)        // [8]
                        TIFFWarningExt(tif->tif_clientdata,module,"ASCII value for tag \"%s\" contains null byte in value; value incorrectly truncated during reading due to implementation limitations",fip->field_name);
                    else if (mb+1>(uint32_t)dp->tdir_count)   // [9]
                    {
                        uint8_t* o;
                        TIFFWarningExt(tif->tif_clientdata,module,"ASCII value for tag \"%s\" does not end in null byte",fip->field_name);
                        if ((uint32_t)dp->tdir_count + 1 != dp->tdir_count + 1)
                            o=NULL;
                        else
                            o=_TIFFmalloc((uint32_t)dp->tdir_count + 1);
                        if (o==NULL)
                        {
                            if (data!=NULL)
                                _TIFFfree(data);
                            return(0);
                        }
                        _TIFFmemcpy(o,data,(uint32_t)dp->tdir_count);
                        o[(uint32_t)dp->tdir_count]=0;
                        if (data!=0)
                            _TIFFfree(data);
                        data=o;
                    }
                    n=TIFFSetField(tif,dp->tdir_tag,data);   // [10]
                    if (data!=0)
                        _TIFFfree(data);
                    if (!n)
                        return(0);
                }
            }

We see that the size of the resultant string is read in at [7] and subsequently checked against the pre-defined LibTiff size that we’re looking for, which for TIFFTAG_DATETIME should be 20 bytes. Assuming the string from the file is smaller [8], a warning is given, and if the string from the file is larger [9], the resultant data is reallocated and truncated. Finally, the actual data is assigned to our resultant TIFF object at [10]. The previous codeflows all contain some aspects that will be important later, but now let us quickly see how libOpenImageIO gets the data out of the TIFF object:

bool tiff_get_string_field(int tag, string_view name OIIO_MAYBE_UNUSED,
                           string_view& result)
{
    auto field = find_field(tag);
    if (!field)
        return false;
    TIFFDataType tiffdatatype = TIFFFieldDataType(field);   // [11]
    int passcount             = TIFFFieldPassCount(field);  // [12]
    int readcount             = TIFFFieldReadCount(field);  // [13]
    // Strutil::printf(" tgsf %s tag %d datatype %d passcount %d readcount %d\n",
    //                 name, tag, int(tiffdatatype), passcount, readcount);
    char* s        = nullptr;
    uint32_t count = 0;
    bool ok        = false;
    if (tiffdatatype == TIFF_ASCII && passcount
        && readcount == TIFF_VARIABLE) {
        uint16_t shortcount = 0;
        ok                  = TIFFGetField(m_tif, tag, &shortcount, &s);
        count               = shortcount;
    } else if (tiffdatatype == TIFF_ASCII && passcount
               && readcount == TIFF_VARIABLE2) {
        ok = TIFFGetField(m_tif, tag, &count, &s);
    } else if (readcount > 0) {                    // [14]
        ok    = TIFFGetField(m_tif, tag, &s);   
        count = readcount;
    } else if (tiffdatatype == TIFF_ASCII) {
        ok = TIFFGetField(m_tif, tag, &s);
        if (ok && s && *s)
            count = strlen(s);
    } else {
        // Some other type, we should not have been asking for this
        // as ASCII, or maybe the tag is just the wrong data type in
        // the file. Punt.
    }
    if (ok && s && *s) {
        result = string_view(s, count);                     // [15]
        // Strip off sometimes-errant extra null characters
        while (result.size() && result.back() == '\0')     
            result.remove_suffix(1);
    }
    return ok;
}

Immediately worth pointing out is that the function calls at [11], [12] and [13] all end up grabbing data from the static LibTiff TIFFField definitions, not the file itself. Thus, if we’re still dealing with TIFFTAG_DATETIME, then tiffdatatype is TIFF_ASCII, passcount is 0x0, and readcount is 20, so we end up hitting the branch at [14], resulting in the count variable always being 20 (at least for TIFFTAG_DATETIME). As such, when we hit the string_view() initializer at [15], the string is created with a length of 20. But if we’ll remember how the data is populated into the field from our file, if our data’s length was less than 20, we only got a warning. So if our tiff file has a TIFFTAG_DATETIME directory with a size that’s less than 20 bytes, we still end up creating a string_view here with a length of 20, resulting in an out-of-bounds read on the heap. With regards to how this data ends up being used, it’s eventually stored back into the libOpenImageIO ImageSpec object as a file attribute, to be used at the discresion of whatever program is utilizing ImageOpen. This vulnerability result in an out of bounds read of adjacent heap data which can contain sensitive process information. Combined with another vulnerability, it could be used as an information leak exploit component used to bypass mitigations.

Crash Information

==98791==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000203 at pc 0x7f04b8eafbc0 bp 0x7ffe741e15f0 sp 0x7ffe741e15e8
READ of size 1 at 0x602000000203 thread T0
    #0 0x7f04b8eafbbf in OpenImageIO_v2_3::TIFFInput::tiff_get_string_field(int, OpenImageIO_v2_3::string_view, OpenImageIO_v2_3::string_view&) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:307:44
    #1 0x7f04b8efc7d1 in OpenImageIO_v2_3::TIFFInput::get_string_attribute(OpenImageIO_v2_3::string_view, int) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:317:13
    #2 0x7f04b8eb0322 in OpenImageIO_v2_3::TIFFInput::find_tag(int, TIFFDataType, OpenImageIO_v2_3::string_view) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:371:13
    #3 0x7f04b8e9dd24 in OpenImageIO_v2_3::TIFFInput::readspec(bool) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:1054:13
    #4 0x7f04b8e8e12b in OpenImageIO_v2_3::TIFFInput::seek_subimage(int, int) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:775:9
    #5 0x7f04b8e8b898 in OpenImageIO_v2_3::TIFFInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec&) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:687:15
    #6 0x7f04b8e92e7c in OpenImageIO_v2_3::TIFFInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec&, OpenImageIO_v2_3::ImageSpec const&) /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:713:12
    #7 0x7f04b7d99669 in OpenImageIO_v2_3::ImageInput::create(OpenImageIO_v2_3::string_view, bool, OpenImageIO_v2_3::ImageSpec const*, OpenImageIO_v2_3::Filesystem::IOProxy*, OpenImageIO_v2_3::string_view) /oiio/oiio-2.3.19.0-unpatched/src/libOpenImageIO/imageioplugin.cpp:758:27
    #8 0x7f04b7c78589 in OpenImageIO_v2_3::ImageInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec const*, OpenImageIO_v2_3::Filesystem::IOProxy*) /oiio/oiio-2.3.19.0-unpatched/src/libOpenImageIO/imageinput.cpp:105:16
    #9 0x56329939840f in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:77:16
    #10 0x5632992be4e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x414e3) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #11 0x5632992a825f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x2b25f) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #12 0x5632992adfb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x30fb6) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #13 0x5632992d7dd2 in main (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x5add2) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #14 0x7f04ae397d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #15 0x7f04ae397e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #16 0x5632992a2b24 in _start (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x25b24) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)

0x602000000203 is located 17 bytes to the right of 2-byte region [0x6020000001f0,0x6020000001f2)
allocated by thread T0 here:
    #0 0x56329935ab5e in malloc (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0xddb5e) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #1 0x7f04ac8f8d8f in setByteArray /build/tiff-sATxYs/tiff-4.3.0/libtiff/tif_dir.c:52:11
    #2 0x7f04ac8f8d8f in setByteArray /build/tiff-sATxYs/tiff-4.3.0/libtiff/tif_dir.c:43:1

SUMMARY: AddressSanitizer: heap-buffer-overflow /oiio/oiio-2.3.19.0-unpatched/src/tiff.imageio/tiffinput.cpp:307:44 in OpenImageIO_v2_3::TIFFInput::tiff_get_string_field(int, OpenImageIO_v2_3::string_view, OpenImageIO_v2_3::string_view&)
Shadow bytes around the buggy address:
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff8000: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fa
  0x0c047fff8010: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff8020: fa fa fd fd fa fa fd fd fa fa 00 00 fa fa fd fa
  0x0c047fff8030: fa fa fd fa fa fa fd fa fa fa fd fa fa fa 02 fa
=>0x0c047fff8040:[fa]fa fd fa fa fa fd fa fa fa 00 fa fa fa fd fa
  0x0c047fff8050: fa fa fd fa fa fa 00 fa fa fa fa fa fa fa fa fa
  0x0c047fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8090: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==98791==ABORTING
TIMELINE

2022-10-19 - Initial Vendor Contact
2022-10-20 - Vendor Disclosure
2022-11-01 - Vendor Patch Release
2022-12-22 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.