Talos Vulnerability Report

TALOS-2022-1629

OpenImageIO RLA format rle span out-of-bounds read vulnerability

December 22, 2022
CVE Number

CVE-2022-36354

SUMMARY

A heap out-of-bounds read vulnerability exists in the RLA format parser of OpenImageIO master-branch-9aeece7a and v2.3.19.0. More specifically, in the way run-length encoded byte spans are handled. A malformed RLA file can lead to an out-of-bounds read of heap metadata which can result in sensitive information leak. 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.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CWE

CWE-193 - Off-by-one Error

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.

The RLA and RLB file formats are quite an outdated set of fileformats, with RLA originally being utilized by Wavefront Technologies ‘The Advanced Visualizer’ back in the 1980’s. Regardless of the vintage, today we see the RLA/RLB formats as one of the file formats supported by LibOpenImageIO. Since this fileformat was designed for animations primarily, it essentially consists of a bunch of images all concatenated together. To start with our analysis, here is some relevant information that we’ll come back to about the file format (from the comments of libOpenImageIO):

/*
  Brief documentation about the RLA format:

  * The file consists of multiple subimages, merely concatenated together.
    Each subimage starts with a RLAHeader, and within the header is a
        NextOffset field that gives the absolute offset (relative to the start
    of the file) of the beginning of the next subimage, or 0 if there
    is no next subimage.     

With that in mind, let’s take a look at what goes on within the RLAHeader:

 RLAHeader m_rla;                   ///< Wavefront RLA header
 // ...

inline bool
RLAInput::read_header()
{
    // Read the image header, which should have the same exact layout as
    // the m_rla structure (except for endianness issues).
    static_assert(sizeof(m_rla) == 740, "Bad RLA struct size");
    if (!read(&m_rla)) {                               // [1]
        errorf("RLA could not read the image header");
        return false;
    }
    m_rla.rla_swap_endian();  // fix endianness

At [1], we see an amazingly simple read directly into the struct. As such, we must dutifully present the structure being read into:

struct RLAHeader {
    int16_t WindowLeft;          // Left side of the full image
    int16_t WindowRight;         // Right side of the full image
    int16_t WindowBottom;        // Bottom of the full image
    int16_t WindowTop;           // Top of the full image
    int16_t ActiveLeft;          // Left side of the viewable image
    int16_t ActiveRight;         // Right side of viewable image
    int16_t ActiveBottom;        // Bottom of the viewable image
    int16_t ActiveTop;           // Top of the viewable image
    int16_t FrameNumber;         // Frame sequence number
    int16_t ColorChannelType;    // Data format of the image channels
    int16_t NumOfColorChannels;  // Number of color channels in image
    int16_t NumOfMatteChannels;  // Number of matte channels in image
    int16_t NumOfAuxChannels;    // Number of auxiliary channels in image
    int16_t Revision;            // File format revision number
    char Gamma[16];              // Gamma setting of image
    char RedChroma[24];          // Red chromaticity
    char GreenChroma[24];        // Green chromaticity
    char BlueChroma[24];         // Blue chromaticity
    char WhitePoint[24];         // White point chromaticity*/
    int32_t JobNumber;           // Job number ID of the file
    char FileName[128];          // Image file name
    char Description[128];       // Description of the file contents
    char ProgramName[64];        // Name of the program that created the file
    char MachineName[32];        // Name of machine used to create the file
    char UserName[32];           // Name of user who created the file
    char DateCreated[20];        // Date the file was created
    char Aspect[24];             // Aspect format of the image
    char AspectRatio[8];         // Aspect ratio of the image
    char ColorChannel[32];       // Format of color channel data
    int16_t FieldRendered;       // Image contains field-rendered data
    char Time[12];               // Length of time used to create the image file
    char Filter[32];             // Name of post-processing filter
    int16_t NumOfChannelBits;    // Number of bits in each color channel pixel
    int16_t MatteChannelType;    // Data format of the matte channels
    int16_t NumOfMatteBits;      // Number of bits in each matte channel pixel
    int16_t AuxChannelType;      // Data format of the auxiliary channels
    int16_t NumOfAuxBits;  // Number of bits in each auxiliary channel pixel
    char AuxData[32];      // Auxiliary channel data description
    char Reserved[36];     // Unused
    int32_t NextOffset;    // Location of the next image header in the file
}

After the above read comes some basic sanity checking and then the reading of the scanline offset table:

inline bool
RLAInput::read_header()
{ // [...]

    if (m_rla.Revision != (int16_t)0xFFFE
        && m_rla.Revision != 0 /* for some reason, this can happen */) {
        errorf("RLA header Revision number unrecognized: %d", m_rla.Revision);
        return false;  // unknown file revision
    }
    if (m_rla.NumOfChannelBits < 0 || m_rla.NumOfChannelBits > 32
        || m_rla.NumOfMatteBits < 0 || m_rla.NumOfMatteBits > 32
        || m_rla.NumOfAuxBits < 0 || m_rla.NumOfAuxBits > 32) {
        errorf("Unsupported bit depth, or maybe corrupted file.");
        return false;
    }
    if (m_rla.NumOfChannelBits == 0)
        m_rla.NumOfChannelBits = 8;  // apparently, this can happen

    // Immediately following the header is the scanline offset table --
    // one uint32_t for each scanline, giving absolute offsets (from the
    // beginning of the file) where the RLE records start for each
    // scanline of this subimage.
    m_sot.resize(std::abs(m_rla.ActiveBottom - m_rla.ActiveTop) + 1, 0); // [2]
    if (!read(&m_sot[0], m_sot.size())) {
        errorf("RLA could not read the scanline offset table");
        return false;
    }
    return true;

The comments are extremely well done and tell us quickly where the image data is that is actually being parsed. When the codeflow finally gets around to reading the scanlines, it reference the std::vector<uint32_t> m_sot; [2] to see where each scanline is located, with each uint32_t corresponding to an absolute offset into the file. This data gets read after all this header parsing, where we briefly return to generic libOpenImageIO code:

bool
ImageInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
                                  int yend, int z, void* data)
{
    // Base class implementation of read_native_scanlines just repeatedly
    // calls read_native_scanline, which is supplied by every plugin.
    // Only the hardcore ones will overload read_native_scanlines with
    // their own implementation.
    lock_guard lock(*this);
    size_t ystride = m_spec.scanline_bytes(true);
    yend           = std::min(yend, spec().y + spec().height);
    for (int y = ybegin; y < yend; ++y) {
        bool ok = read_native_scanline(subimage, miplevel, y, z, data);
        if (!ok)
            return false;
        data = (char*)data + ystride;
    }
    return true;
}

Since our image is an RLA file, we then enter the RLA-specific implimentation:

bool
RLAInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
                               void* data)
{
    lock_guard lock(*this);
    if (!seek_subimage(subimage, miplevel))
        return false;

    // By convention, RLA images store their images bottom-to-top.
    y = m_spec.height - (y - m_spec.y) - 1;

    // Seek to scanline start, based on the scanline offset table
    fseek(m_file, m_sot[y], SEEK_SET);   // [3]

    // Now decode and interleave the channels.
    // The channels are non-interleaved (i.e. rrrrrgggggbbbbb...).
    // Color first, then matte, then auxiliary channels.  We can't
    // decode all in one shot, though, because the data type and number
    // of significant bits may be may be different for each class of
    // channels, so we deal with them separately and interleave into
    // our buffer as we go.
    size_t size = m_spec.scanline_bytes(true);  // if (width < 0) ? 0; clamped_mult6s4((imagesize_t)width, (imagesize_t)pixel_bytes(native));
    m_buf.resize(size);
    if (m_rla.NumOfColorChannels > 0)                        // [4]
        if (!decode_channel_group(0, m_rla.NumOfColorChannels,
                                  m_rla.NumOfChannelBits, y))
            return false;
    if (m_rla.NumOfMatteChannels > 0)                        // [5]
        if (!decode_channel_group(m_rla.NumOfColorChannels,
                                  m_rla.NumOfMatteChannels,
                                  m_rla.NumOfMatteBits, y))
            return false;
    if (m_rla.NumOfAuxChannels > 0)                          // [6]
        if (!decode_channel_group(m_rla.NumOfColorChannels
                                      + m_rla.NumOfMatteChannels,
                                  m_rla.NumOfAuxChannels, m_rla.NumOfAuxBits,
                                  y))
            return false;

    memcpy(data, &m_buf[0], size); //[7]
    return true;
}

Each subimage is treated the same, and read in data starting from the designated scanline offset table at [3]. All the different channel types are read in at [4], [5] and [6], before finally being memcpy’ed into the final subimage at [7]. For the purposes of this vulnerability however, we strictly only care about the calls to decode_channel_group:

bool
RLAInput::decode_channel_group(int first_channel, short num_channels,    // 32 == max channels
                               short num_bits, int y)                    // first_channel == 1, 2, or 3
{
    // Some preliminaries -- figure out various sizes and offsets
    int chsize;         // size of the channels in this group, in bytes
    int offset;         // buffer offset to first channel
    int pixelsize;      // spacing between pixels (in bytes) in the output
    TypeDesc chantype;  // data type for the channel
    if (!m_spec.channelformats.size()) {
        // No per-channel formats, they are all the same, so it's easy
        chantype  = m_spec.format;
        chsize    = chantype.size();
        offset    = first_channel * chsize;
        pixelsize = chsize * m_spec.nchannels;
    } else {
    // [...]
    }

    // Read the big-endian values into the buffer.
    // The channels are simply concatenated together in order.
    // Each channel starts with a length, from which we know how many
    // bytes of encoded RLE data to read.  Then there are RLE
    // spans for each 8-bit slice of the channel.
    std::vector<char> encoded;
    for (int c = 0; c < num_channels; ++c) {
        // Read the length
        uint16_t lenu16;  // number of encoded bytes
        if (!read(&lenu16)) {                                       // [8]
            errorf("Read error: couldn't read RLE record length");
            return false;
        }
        size_t length = lenu16;
        // Read the encoded RLE record
        encoded.resize(length);                           
        if (!read(&encoded[0], length)) {                           // [9]
            errorf("Read error: couldn't read RLE data span");
            return false;
        }

        if (chantype == TypeDesc::FLOAT) {
            // [...]
        }

        // Decode RLE -- one pass for each significant byte of the file,
        // which we re-interleave properly by passing the right offsets
        // and strides to decode_rle_span.
        size_t eoffset = 0;
        for (int bytes = 0; bytes < chsize && length > 0; ++bytes) {
            size_t e = decode_rle_span(&m_buf[offset + c * chsize + bytes],  // [10]
                                       m_spec.width, pixelsize,
                                       &encoded[eoffset], length);
            if (!e)
                return false;
            eoffset += e;
            length -= e;
        }r
    }

The length of our channel is read in at the offset we fseek’ed to earlier [8], and then we read that many bytes directly into an allocated buffer [9]. Assuming that we’re not dealing with floats, then the data is RLE compressed and is handled by the decode_rle_span function at [10]:

size_t
RLAInput::decode_rle_span(unsigned char* buf, int n, int stride,
                          const char* encoded, size_t elen)
{
    size_t e = 0;
    while (n > 0 && e < elen) {  // [11]
        signed char count = (signed char)encoded[e++]; //[12]
        if (count >= 0) {
            // run count positive: value repeated count+1 times
            for (int i = 0; i <= count && n; ++i, buf += stride, --n)
                *buf = encoded[e];     // [13] 
            ++e;
        } else {
            // run count negative: repeat bytes literally
            count = -count;  // make it positive
            for (; count && n > 0 && e < elen; --count, buf += stride, --n)
                *buf = encoded[e++];
        }
    }
    if (n != 0) {
        errorf("Read error: malformed RLE record");
        return 0;
    }
    return e;
}

Finally we reach the location of the vulnerabiity. Within our loop at [11], there are two conditions keeping it going. First, the n variable (which corresponds to the m_spec.width header field) must be > 0, and the e counter must be less than the elen variable (which corresponds to the length of our input scanline, at max a uint16_t). To quickly explain the bug, the line at [12] ends up iterating the e variable immediately after the check, so if e == elen-1 then after the e++, e is equal to the length of the buffer. Since it’s an array (that starts from 0x0), we end up with an off-by-one condition. Thus, when the read at [13] occurs, we are actually reading out of bounds, which results in us writing heap data into our output buffer. While it ends up leaking only one byte, decode_rle_span ends up being called a great deal of times, and as such can result in a non-trivial amount of heap data being leaked. Combined with another vulnerability, it could be used as an information leak exploit component used to bypass mitigations.

Crash Information

=================================================================
==216036==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60400000057f at pc 0x7ffff34d1334 bp 0x7fffffff9000 sp 0x7fffffff8ff8
READ of size 1 at 0x60400000057f thread T0
[Detaching after fork from child process 216040]
    #0 0x7ffff34d1333 in OpenImageIO_v2_3::RLAInput::decode_rle_span(unsigned char*, int, int, char const*, unsigned long) /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:493:24
    #1 0x7ffff34d4989 in OpenImageIO_v2_3::RLAInput::decode_channel_group(int, short, short, int) /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:576:24
    #2 0x7ffff34db796 in OpenImageIO_v2_3::RLAInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:670:14
    #3 0x7ffff24d0a88 in OpenImageIO_v2_3::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:392:19
    #4 0x7ffff24d17d7 in OpenImageIO_v2_3::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:413:16                                                                               
    #5 0x7ffff24cd243 in OpenImageIO_v2_3::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:329:15
    #6 0x7ffff24ef90c in OpenImageIO_v2_3::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:959:23
    #7 0x7ffff24e9b67 in OpenImageIO_v2_3::ImageInput::read_image(int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:875:12
    #8 0x7ffff24e925e in OpenImageIO_v2_3::ImageInput::read_image(OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:857:12
    #9 0x55555566fa58 in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:89:18
    #10 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x414e3) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #11 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x2b25f) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #12 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x30fb6) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #13 0x5555555aedd2 in main (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x5add2) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #14 0x7fffe8bddd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #15 0x7fffe8bdde3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #16 0x555555579b24 in _start (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x25b24) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)

0x60400000057f is located 0 bytes to the right of 47-byte region [0x604000000550,0x60400000057f)
allocated by thread T0 here:
    #0 0x55555566c90d in operator new(unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x11890d) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #1 0x7fffed361011 in __gnu_cxx::new_allocator<char>::allocate(unsigned long, void const*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:127:27
    #2 0x7fffed360a23 in std::allocator_traits<std::allocator<char> >::allocate(std::allocator<char>&, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:464:20
    #3 0x7fffef82871b in std::_Vector_base<char, std::allocator<char> >::_M_allocate(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:346:20
    #4 0x7ffff1c070b1 in std::vector<char, std::allocator<char> >::_M_default_append(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/vector.tcc:635:34
    #5 0x7ffff1bba69c in std::vector<char, std::allocator<char> >::resize(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:940:4
    #6 0x7ffff34d31a8 in OpenImageIO_v2_3::RLAInput::decode_channel_group(int, short, short, int) /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:551:17
    #7 0x7ffff34db796 in OpenImageIO_v2_3::RLAInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:670:14
    #8 0x7ffff24d0a88 in OpenImageIO_v2_3::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:392:19
    #9 0x7ffff24d17d7 in OpenImageIO_v2_3::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:413:16
    #10 0x7ffff24cd243 in OpenImageIO_v2_3::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:329:15
    #11 0x7ffff24ef90c in OpenImageIO_v2_3::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:959:23
    #12 0x7ffff24e9b67 in OpenImageIO_v2_3::ImageInput::read_image(int, int, OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:875:12
    #13 0x7ffff24e925e in OpenImageIO_v2_3::ImageInput::read_image(OpenImageIO_v2_3::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:857:12
    #14 0x55555566fa58 in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:89:18
    #15 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x414e3) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #16 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x2b25f) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #17 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x30fb6) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #18 0x5555555aedd2 in main (/oiio/fuzzing/unpatched_setup/fuzz_oiio.bin+0x5add2) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #19 0x7fffe8bddd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /oiio/oiio-2.3.19.0/src/rla.imageio/rlainput.cpp:493:24 in OpenImageIO_v2_3::RLAInput::decode_rle_span(unsigned char*, int, int, char const*, unsigned long)
Shadow bytes around the buggy address:
  0x0c087fff8050: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
  0x0c087fff8060: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fa
  0x0c087fff8070: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
  0x0c087fff8080: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
  0x0c087fff8090: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
=>0x0c087fff80a0: fa fa fd fd fd fd fd fa fa fa 00 00 00 00 00[07]
  0x0c087fff80b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c087fff80c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c087fff80d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c087fff80e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c087fff80f0: 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):
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.