Talos Vulnerability Report

TALOS-2022-1633

OpenImageIO TIFF tile pels decoding heap-based buffer overflow

December 22, 2022
CVE Number

CVE-2022-41639

SUMMARY

A heap based buffer overflow vulnerability exists in tile decoding code of TIFF image parser in OpenImageIO master-branch-9aeece7a and v2.3.19.0. A specially-crafted TIFF file can lead to an out of bounds memory corruption, which can result in arbitrary code execution. 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

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

CWE

CWE-122 - Heap-based Buffer Overflow

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:

00000000   49 49 2A 00  08 00 00 00  18 00 00 01  03 00 01 00  II*............. // [2]
00000010   00 00 80 00  00 00 01 01  03 00 01 00  00 00 00 02  ................
00000020   00 00 02 01  03 00 03 00  00 00 2E 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 12 01  03 00 01 00  00 00 01 00  ................
00000050   00 00 15 01  03 00 01 00  00 00 03 00  00 00 16 01  ................
00000060   03 00 01 00  00 00 20 00  00 00 1C 01  03 00 01 00  ...... .........
00000070   00 00 01 00  00 00 1E 01  05 00 01 00  00 00 34 01  ..............4.
00000080   00 00 1F 01  05 00 01 00  00 00 3C 01  00 00 31 01  ..........<...1.
00000090   02 00 23 00  00 00 44 01  00 00 32 01  02 00 14 00  ..#...D...2.....
000000A0   00 00 68 01  00 00 3D 01  03 00 01 00  00 00 02 00  ..h...=.........
000000B0   00 00 42 01  03 00 01 00  00 00 40 00  00 00 43 01  ..B.......@...C.
000000C0   03 00 01 00  00 00 40 00  00 00 44 01  04 00 10 00  ......@...D.....
000000D0   00 00 7C 01  00 00 45 01  04 00 10 00  00 00 BC 01  ..|...E.........
000000E0   00 00 53 01  03 00 03 00  00 00 FC 01  00 00 BC 02  ..S.............
000000F0   01 00 24 02  00 00 02 02  00 00 16 82  02 00 0E 00  ..$.............
00000100   00 00 26 04  00 00 17 82  02 00 0C 00  00 00 34 04  ..&...........4.
00000110   00 00 18 82  0B 00 01 00  00 00 00 00  80 3E BB 83  .............>..
00000120   04 00 0A 00  00 00 40 04  00 00 F8 09  00 00 08 00  ......@.........


#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, i.e. 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 0x18. 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 0x18 * 0xc, resulting in 0x120 bytes being read. In the hexdump above, this would mean that our directories come directly from the bytes at offset (0xA, 0x12A). For example, if we look at the directories at offset 0x8E get parsed, we see the following:

00000080                                             31 01  |..........<...1.|
00000090  02 00 23 00 00 00 44 01  00 00 // start of second directory
                                         32 01 02 00 14 00  |..#...D...2.....|
000000a0  00 00 68 01 00 00

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

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

Regardless, after populating all these directories into memory, futher processing occurs on some of the more pivotal directories inside LibTiff’s TIFFReadDirectory(TIFF *tif). An example of this looks like so:

    int TIFFReadDirectory(TIFF* tif) {
    static const char module[] = "TIFFReadDirectory";
     // [...]
     
            dp=TIFFReadDirectoryFindEntry(tif,dir,dircount,TIFFTAG_SAMPLESPERPIXEL);
            if (dp)
            {
                if (!TIFFFetchNormalTag(tif,dp,0))
                    goto bad;
                dp->tdir_ignore = TRUE;
            }

A few passes are done over all the directories to search for specific information, including such important information as the size of the tiff file’s tiles:

#define TIFFTAG_TILEWIDTH       322 /* !tile width in pixels */
#define TIFFTAG_TILELENGTH      323 /* !tile height in pixels */
#define TIFFTAG_TILEOFFSETS     324 /* !offsets to data tiles */
#define TIFFTAG_TILEBYTECOUNTS      325 /* !byte counts for tiles */

 */
for (di=0, dp=dir; di<dircount; di++, dp++)
{
    // [...]
    if (!dp->tdir_ignore)
    {
        fip=tif->tif_fields[fii];
        if (fip->field_bit==FIELD_IGNORE)
            dp->tdir_ignore = TRUE;
        else
        {
            switch (dp->tdir_tag)
            {
                case TIFFTAG_STRIPOFFSETS:
                case TIFFTAG_STRIPBYTECOUNTS:
                case TIFFTAG_TILEOFFSETS:
                case TIFFTAG_TILEBYTECOUNTS:
                    TIFFSetFieldBit(tif,fip->field_bit);
                    break;
                case TIFFTAG_IMAGEWIDTH:
                case TIFFTAG_IMAGELENGTH:
                case TIFFTAG_IMAGEDEPTH:
                case TIFFTAG_TILELENGTH:                   // [5]
                case TIFFTAG_TILEWIDTH:
                case TIFFTAG_TILEDEPTH:
                case TIFFTAG_PLANARCONFIG:
                case TIFFTAG_ROWSPERSTRIP:
                case TIFFTAG_EXTRASAMPLES:
                    if (!TIFFFetchNormalTag(tif,dp,0))   
                        goto bad;
                    dp->tdir_ignore = TRUE;
                    break;
                default:
                    if( !_TIFFCheckFieldIsValidForCodec(tif, dp->tdir_tag) )
                        dp->tdir_ignore = TRUE;
                    break;
            }

At [5], we see that the TIFFTAG_TILELENGTH field (tag 0x141) is gathered and populated into tif->dir.td_tilelength, which will be important later. After this, we eventually calculate the size of the tif->tif_tilesize and the tif->scanlinesize, both of which determine the sizes of temporary buffers when reading in the image data:

    int TIFFReadDirectory(TIFF* tif) {
    static const char module[] = "TIFFReadDirectory";
     // [...]
     
     
    /*
     * Reinitialize i/o since we are starting on a new directory.
     */
    tif->tif_row = (uint32_t) -1;
    tif->tif_curstrip = (uint32_t) -1;
    tif->tif_col = (uint32_t) -1;
    tif->tif_curtile = (uint32_t) -1;
    tif->tif_tilesize = (tmsize_t) -1;

    tif->tif_scanlinesize = TIFFScanlineSize(tif);
    if (!tif->tif_scanlinesize) {
        TIFFErrorExt(tif->tif_clientdata, module,
            "Cannot handle zero scanline size");
        return (0);
    }

    if (isTiled(tif)) {
        tif->tif_tilesize = TIFFTileSize(tif);    //[6]
        if (!tif->tif_tilesize) {
            TIFFErrorExt(tif->tif_clientdata, module,
                 "Cannot handle zero tile size");
            return (0);
        }
    } else {
        if (!TIFFStripSize(tif)) {
            TIFFErrorExt(tif->tif_clientdata, module,
                "Cannot handle zero strip size");
            return (0);
        }
        }

At [6], we see where the TIF object’s tif_tilesize is generated, and the codeflow looks as such:

/*
 * Compute the # bytes in a row-aligned tile.
 */
uint64_t
TIFFTileSize64(TIFF* tif)
{
    return (TIFFVTileSize64(tif, tif->tif_dir.td_tilelength));  // [7]
}
tmsize_t
TIFFTileSize(TIFF* tif)
{
    static const char module[] = "TIFFTileSize";
    uint64_t m;
    m=TIFFTileSize64(tif);
    return _TIFFCastUInt64ToSSize(tif, m, module);
}

As shown at [7], the tif->tilesize is directly correlated to the tif_dir.td_tilelength, which was read directly from our tif->tif_dir above. Continuing into TIFFVTileSize64:

uint64_t
TIFFVTileSize64(TIFF* tif, uint32_t nrows)
{
    static const char module[] = "TIFFVTileSize64";
    TIFFDirectory *td = &tif->tif_dir;
    if (td->td_tilelength == 0 || td->td_tilewidth == 0 ||
        td->td_tiledepth == 0)
        return (0);
    if ((td->td_planarconfig==PLANARCONFIG_CONTIG)&&
        (td->td_photometric==PHOTOMETRIC_YCBCR)&&
        (td->td_samplesperpixel==3)&&
        (!isUpSampled(tif)))
    {
    // [...]
    
        else
    return(_TIFFMultiply64(tif,nrows,TIFFTileRowSize64(tif),module));  // [8] }

Assuming the tif->tif_dir passes some sanity checks, we make it down to [8], where we see our tif_dir.td_tilelength gets multiplied by the return value of TIFFTileRowSize64:

/*
 * Compute the # bytes in each row of a tile.
 */
uint64_t
TIFFTileRowSize64(TIFF* tif)
{
    static const char module[] = "TIFFTileRowSize64";
    TIFFDirectory *td = &tif->tif_dir;
    uint64_t rowsize;
    uint64_t tilerowsize;

    if (td->td_tilelength == 0)
        {
                TIFFErrorExt(tif->tif_clientdata,module,"Tile length is zero");
                return 0;
        }
        if (td->td_tilewidth == 0)
        {
                TIFFErrorExt(tif->tif_clientdata,module,"Tile width is zero");
        return (0);
        }
    rowsize = _TIFFMultiply64(tif, td->td_bitspersample, td->td_tilewidth,       // [9]
        "TIFFTileRowSize");
    if (td->td_planarconfig == PLANARCONFIG_CONTIG)
        {
                if (td->td_samplesperpixel == 0)
                {
                        TIFFErrorExt(tif->tif_clientdata,module,"Samples per pixel is zero");
                        return 0;
                }
        rowsize = _TIFFMultiply64(tif, rowsize, td->td_samplesperpixel,            // [10]
            "TIFFTileRowSize");
        }
        tilerowsize=TIFFhowmany8_64(rowsize);          // [11]
        if (tilerowsize == 0)
        {
                TIFFErrorExt(tif->tif_clientdata,module,"Computed tile row size is zero");
                return 0;
        }
    return (tilerowsize);
}

We see that the final determinant of the size of the buffer ends up being tif_dir.td_tilelength multiplied against the tilerowsize variable, which is calculated by further multiplications of assorted tiff tags. TIFFTAG_BITSPERSAMPLE * TIFFTAG_TILEWIDTH [9] further optionally multiplied against the TIFFTAG_SAMPLESPERPIXEL [10] and then run into the TIFFhowmany8_64 function at [11]:

tiffiop.h:#define TIFFhowmany8_64(x) (((x)&0x07)?((uint64_t)(x)>>3)+1:(uint64_t)(x)>>3)

Regardless, after all that, we clearly see that the final tif->tif_tilesize[6] is calculated from assorted TIFFTAG fields, all from the tiff input file itself. For a tiff file with a td_tilelength of 0x40 and a td_tilewidth of 0x40, as well as a td_bitspersample of 0x8, we end up with tif->tif_tilesize being 0x4000. Carrying on further into the code after that delightful tangent, let us examine where this tif->tif_tilesize actually matters:

bool
ImageInput::read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
                              int ybegin, int yend, int zbegin, int zend,
                              void* data)
{
    // [...]
    ImageSpec spec = spec_dimensions(subimage, miplevel);  // thread-safe  // [12]
    // [...]

    // Base class implementation of read_native_tiles just repeatedly
    // calls read_native_tile, which is supplied by every plugin that
    // supports tiles.  Only the hardcore ones will overload
    // read_native_tiles with their own implementation.
    stride_t pixel_bytes = (stride_t)spec.pixel_bytes(true);
    stride_t tileystride = pixel_bytes * spec.tile_width;
    stride_t tilezstride = tileystride * spec.tile_height;
    stride_t ystride     = (xend - xbegin) * pixel_bytes;
    stride_t zstride     = (yend - ybegin) * ystride;
    std::unique_ptr<char[]> pels(new char[spec.tile_bytes(true)]);    // [13]
    for (int z = zbegin; z < zend; z += spec.tile_depth) {
        for (int y = ybegin; y < yend; y += spec.tile_height) {
            for (int x = xbegin; x < xend; x += spec.tile_width) {
                bool ok = read_native_tile(subimage, miplevel, x, y, z,
                                           &pels[0]);

At [12] we generate an ImageSpec spec object that is populated from the same file data as before, and at [13] this ImageSpec object is used to generate a variable length buffer via the tile_bytes class method:

imagesize_t
ImageSpec::tile_pixels() const noexcept
{
    if (tile_width <= 0 || tile_height <= 0 || tile_depth <= 0)
        return 0;
    imagesize_t r = clamped_mult64((imagesize_t)tile_width,  // [14]
                                   (imagesize_t)tile_height);
    if (tile_depth > 1)
        r = clamped_mult64(r, (imagesize_t)tile_depth);
    return r;
}



imagesize_t
ImageSpec::tile_bytes(bool native) const noexcept
{
    return clamped_mult64(tile_pixels(), (imagesize_t)pixel_bytes(native));
}

It’s worth noting that these tile_width and tile_height variables around [14] can be populated with the same 0x40 and 0x40 that we saw before, and if our tile depth ends up being 0x1 (which it can be), then the return value of tile_pixels() is 0x1000, which then gets multiplied against the return value of pixel_bytes(true):

size_t
ImageSpec::pixel_bytes(bool native) const noexcept
{
    if (nchannels < 0)
        return 0;
    if (!native || channelformats.empty())
        return clamped_mult32((size_t)nchannels, channel_bytes()); // [15]
    else {
        size_t sum = 0;
        for (int i = 0; i < nchannels; ++i)
            sum += channelformats[i].size();
        return sum;
    }
}

There is a hard limit for the number of channels, so there is not much variation in the return value here. Assuming all of the channels in the tif file are the same, we hit the branch at [15]. This causes the nchannels (which for us is 0x3) to be multiplied against the spec.format.size(), which in our case is 0x1. As such, the resultant size of the pels vector at [13] is 0x3 * 0x40 * 0x40, or 0x3000. And if we look at what ends up happening to this pels vector, we eventually run into an issue:

  bool
    ImageInput::read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
                                  int ybegin, int yend, int zbegin, int zend,
                                  void* data)
    {
        // [...]
        ImageSpec spec = spec_dimensions(subimage, miplevel);  // thread-safe  
        // [...]

        std::unique_ptr<char[]> pels(new char[spec.tile_bytes(true)]);   
        for (int z = zbegin; z < zend; z += spec.tile_depth) {
            for (int y = ybegin; y < yend; y += spec.tile_height) {
                for (int x = xbegin; x < xend; x += spec.tile_width) {
                    bool ok = read_native_tile(subimage, miplevel, x, y, z,    // [16]
                                               &pels[0]);

At [16], this vector is passed into the tiff-specific read_native_tile function as the temporary buffer data. Continuing into read_native_tile:

bool
TIFFInput::read_native_tile(int subimage, int miplevel, int x, int y, int z,
                            void* data)
{
    lock_guard lock(*this);
    if (!seek_subimage(subimage, miplevel))
        return false;
    x -= m_spec.x;
    y -= m_spec.y;

    // [...] 
    
    imagesize_t tile_pixels = m_spec.tile_pixels();
    imagesize_t nvals       = tile_pixels * m_spec.nchannels;
    if (m_photometric == PHOTOMETRIC_PALETTE && m_bitspersample > 8)
        m_scratch.resize(nvals * 2);  // special case for 16 bit palette
    else
        m_scratch.resize(m_spec.tile_bytes());                                      // [17]
    bool no_bit_convert = (m_bitspersample == 8 || m_bitspersample == 16
                           || m_bitspersample == 32);
    if (m_photometric == PHOTOMETRIC_PALETTE) {
            // [...]
    } else {
        // Not palette
        imagesize_t plane_bytes = m_spec.tile_pixels() * m_spec.format.size();
        int planes              = m_separate ? m_spec.nchannels : 1;
        std::vector<unsigned char> scratch2(m_separate ? m_spec.tile_bytes()
                                                       : 0);
        // Where to read?  Directly into user data if no channel shuffling
        // or bit shifting is needed, otherwise into scratch space.
        unsigned char* readbuf = (no_bit_convert && !m_separate)                     // [18]
                                     ? (unsigned char*)data
                                     : &m_scratch[0];


        // Perform the reads.  Note that for contig, planes==1, so it will
        // only do one TIFFReadTile.
        for (int c = 0; c < planes; ++c) /* planes==1 for contig */
            if (TIFFReadTile(m_tif, &readbuf[plane_bytes * c], x, y, z, c)             // [19]
                < 0) {
                errorf("%s", oiio_tiff_last_error());
                return false;
            }

We finally get to the crux of the matter in which we finally start to read Tiff tiles into our temporary readbuf at [19]. Interestingly however, the readbuf pointer can either point at the input data buffer (our pels vector from before), or the m_scratch array [18]. The array’s size is determined around [17], as the tile_bytes() function will resize the m_scratch buffer to 0x4000 bytes, whereas our pels vector is only length 0x3000. Continuing on into TIFFReadTile:

 /*
 * Read and decompress a tile of data.  The
 * tile is selected by the (x,y,z,s) coordinates.
 */
tmsize_t
TIFFReadTile(TIFF* tif, void* buf, uint32_t x, uint32_t y, uint32_t z, uint16_t s)
{
    if (!TIFFCheckRead(tif, 1) || !TIFFCheckTile(tif, x, y, z, s))
        return ((tmsize_t)(-1));
    return (TIFFReadEncodedTile(tif,
        TIFFComputeTile(tif, x, y, z, s), buf, (tmsize_t)(-1)));  // [20]
}

The above code leads into TIFFComputeTile at [20]:

/*
 * Read a tile of data and decompress the specified
 * amount into the user-supplied buffer.
 */
tmsize_t
TIFFReadEncodedTile(TIFF* tif, uint32_t tile, void* buf, tmsize_t size)
{
    static const char module[] = "TIFFReadEncodedTile";
    TIFFDirectory *td = &tif->tif_dir;
    tmsize_t tilesize = tif->tif_tilesize;                 // [21]

    // [...]

    if (TIFFFillTile(tif, tile) && (*tif->tif_decodetile)(tif,
                                                          (uint8_t*) buf, size, (uint16_t)(tile / td->td_stripsperimage))) { // [22]
        (*tif->tif_postdecode)(tif, (uint8_t*) buf, size);
        return (size);
    } else
        return ((tmsize_t)(-1));
}

Since -1 is passed in as our size tmsize_t size argument, we end up using the tilesize variable at [21]. This is clearly assigned as tif->tif_tilesize, which we thoroughly defined as 0x4000. Eventually this gets passed in as the size of our buffer at tif->tif_decodetile(). Our pels vector can be the destination buffer, which has a size of 0x3000, eventually resulting in a heap overflow. At least in our instance PoC, the tif->m_scratch vector follows pretty close after the pels vector. Its heap metadata is corrupted before triggering an ASAN crash when the tif object is eventually destroyed and the tif->m_scratch vector is freed. With precise memory layout control and heap metadata manipulation, this memory corruption can lead to arbitrary code execution.

Crash Information

==124926==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x627000003900 in thread T0
[Detaching after fork from child process 124937]
    #0 0x55555566d16d in operator delete(void*) (/oiio/fuzzing_master/fuzz_oiio.bin+0x11916d) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #1 0x7ffff13ebebf in __gnu_cxx::new_allocator<unsigned char>::deallocate(unsigned char*, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:145:2
    #2 0x7ffff13ebdff in std::allocator_traits<std::allocator<unsigned char> >::deallocate(std::allocator<unsigned char>&, unsigned char*, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:496:13
    #3 0x7ffff13ea81a in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::_M_deallocate(unsigned char*, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:354:4
    #4 0x7ffff13ec981 in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::~_Vector_base() /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:335:2
    #5 0x7ffff13e6816 in std::vector<unsigned char, std::allocator<unsigned char> >::~vector() /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:683:7
    #6 0x7ffff3ddb6a5 in OpenImageIO_v2_5_0::TIFFInput::~TIFFInput() /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:634:1
    #7 0x7ffff3ddba91 in OpenImageIO_v2_5_0::TIFFInput::~TIFFInput() /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:631:1
    #8 0x555555670850 in std::default_delete<OpenImageIO_v2_5_0::ImageInput>::operator()(OpenImageIO_v2_5_0::ImageInput*) const /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:85:2
    #9 0x5555556705de in std::unique_ptr<OpenImageIO_v2_5_0::ImageInput, std::default_delete<OpenImageIO_v2_5_0::ImageInput> >::~unique_ptr() /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:361:4
    #10 0x55555566fd73 in LLVMFuzzerTestOneInput /oiio/fuzzing_master/./oiio_harness.cpp:99:1
    #11 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x414e3) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #12 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x2b25f) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #13 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_master/fuzz_oiio.bin+0x30fb6) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #14 0x5555555aedd2 in main (/oiio/fuzzing_master/fuzz_oiio.bin+0x5add2) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #15 0x7fffec30bd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #16 0x7fffec30be3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #17 0x555555579b24 in _start (/oiio/fuzzing_master/fuzz_oiio.bin+0x25b24) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)

0x627000003900 is located 2048 bytes to the right of 12288-byte region [0x627000000100,0x627000003100)
freed by thread T0 here:
    #0 0x55555566d26d in operator delete[](void*) (/oiio/fuzzing_master/fuzz_oiio.bin+0x11926d) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #1 0x7ffff2b871a6 in std::enable_if<is_convertible<char (*) [], char (*) []>::value, void>::type std::default_delete<char []>::operator()<char>(char*) const /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:120:4
    #2 0x7ffff2a32af3 in std::unique_ptr<char [], std::default_delete<char []> >::~unique_ptr() /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:612:4
    #3 0x7ffff2d59aa2 in OpenImageIO_v2_5_0::ImageInput::read_native_tiles(int, int, int, int, int, int, int, int, void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:789:1
    #4 0x7ffff3e40633 in OpenImageIO_v2_5_0::TIFFInput::read_native_tiles(int, int, int, int, int, int, int, int, void*) /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:2129:28
    #5 0x7ffff2d5115f in OpenImageIO_v2_5_0::ImageInput::read_tiles(int, int, int, int, int, int, int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:620:20
    #6 0x7ffff3e4bcaf in OpenImageIO_v2_5_0::TIFFInput::read_tiles(int, int, int, int, int, int, int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long) /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:2298:27
    #7 0x7ffff2d61270 in OpenImageIO_v2_5_0::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:941:23
    #8 0x7ffff2d5cb5a in OpenImageIO_v2_5_0::ImageInput::read_image(OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:864:12
    #9 0x55555566fa58 in LLVMFuzzerTestOneInput /oiio/fuzzing_master/./oiio_harness.cpp:89:18
    #10 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x414e3) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #11 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x2b25f) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #12 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_master/fuzz_oiio.bin+0x30fb6) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #13 0x5555555aedd2 in main (/oiio/fuzzing_master/fuzz_oiio.bin+0x5add2) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #14 0x7fffec30bd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

previously allocated by thread T0 here:
    #0 0x55555566ca1d in operator new[](unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x118a1d) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #1 0x7ffff2d586ff in OpenImageIO_v2_5_0::ImageInput::read_native_tiles(int, int, int, int, int, int, int, int, void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:770:34
    #2 0x7ffff3e40633 in OpenImageIO_v2_5_0::TIFFInput::read_native_tiles(int, int, int, int, int, int, int, int, void*) /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:2129:28
    #3 0x7ffff2d5115f in OpenImageIO_v2_5_0::ImageInput::read_tiles(int, int, int, int, int, int, int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:620:20
    #4 0x7ffff3e4bcaf in OpenImageIO_v2_5_0::TIFFInput::read_tiles(int, int, int, int, int, int, int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long) /oiio/oiio-master/src/tiff.imageio/tiffinput.cpp:2298:27
    #5 0x7ffff2d61270 in OpenImageIO_v2_5_0::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:941:23
    #6 0x7ffff2d5cb5a in OpenImageIO_v2_5_0::ImageInput::read_image(OpenImageIO_v2_5_0::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-master/src/libOpenImageIO/imageinput.cpp:864:12
    #7 0x55555566fa58 in LLVMFuzzerTestOneInput /oiio/fuzzing_master/./oiio_harness.cpp:89:18
    #8 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x414e3) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #9 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_master/fuzz_oiio.bin+0x2b25f) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #10 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_master/fuzz_oiio.bin+0x30fb6) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #11 0x5555555aedd2 in main (/oiio/fuzzing_master/fuzz_oiio.bin+0x5add2) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2)
    #12 0x7fffec30bd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: bad-free (/oiio/fuzzing_master/fuzz_oiio.bin+0x11916d) (BuildId: 3723dfbe3c255f3559fd21ebe0474ec771515df2) in operator delete(void*)
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.