Talos Vulnerability Report

TALOS-2023-1879

libigl PlyFile ply_cast_ascii out-of-bounds write vulnerability

May 28, 2024
CVE Number

CVE-2023-49600

SUMMARY

An out-of-bounds write vulnerability exists in the PlyFile ply_cast_ascii functionality of libigl v2.5.0. A specially crafted .ply file can lead to a heap buffer overflow. 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.

libigl v2.5.0

PRODUCT URLS

libigl - https://libigl.github.io/

CVSSv3 SCORE

8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-122 - Heap-based Buffer Overflow

DETAILS

libigl is a C++ geometry processing library that is designed to be simple to integrate into projects using a header-only construction for the code base. This library is widely utilized in industries ranging from Triple-A game development to 3D printing, and it can be found in many applications that require the geometry processing of various file formats.

A heap buffer overflow exists within the ply_cast_ascii functionality of libIGL. This function is simple and stores data into a destination buffer from a stream.

template<typename T> void ply_cast_ascii(void * dest, std::istream & is)
{
    *(static_cast<T *>(dest)) = ply_read_ascii<T>(is);
} 

ply_read_ascii is equally simple retrieving data from the stream.

template<typename T> IGL_INLINE T ply_read_ascii(std::istream & is)
{
    T data;
    is >> data;
    return data;
} 

ply_cast_ascii is called from read_property_ascii, which is responsible for parsing properties associated with elements from the PLY file format header (only in the ASCII format). Each type of this template could be used to trigger this vulnerability, but the root cause is elsewhere in the codebase.

IGL_INLINE size_t PlyFile::PlyFileImpl::read_property_ascii(const Type & t, const size_t & stride, void * dest, size_t & destOffset, std::istream & is)
{
    destOffset += stride;
    switch (t)
    {
    case Type::INT8:       *((int8_t *)dest)  = static_cast<int8_t>(ply_read_ascii<int32_t>(is));   break;
    case Type::UINT8:      *((uint8_t *)dest) = static_cast<uint8_t>(ply_read_ascii<uint32_t>(is)); break
    case Type::INT16:      ply_cast_ascii<int16_t>(dest, is);                 break;
    case Type::UINT16:     ply_cast_ascii<uint16_t>(dest, is);                break;
    case Type::INT32:      ply_cast_ascii<int32_t>(dest, is);                 break;
    case Type::UINT32:     ply_cast_ascii<uint32_t>(dest, is);                break;
    case Type::FLOAT32:    ply_cast_ascii<float>(dest, is);                   break;
    case Type::FLOAT64:    ply_cast_ascii<double>(dest, is);                  break;
    case Type::INVALID:    throw std::invalid_argument("invalid ply property");
    }
    return stride;
}

read_property_ascii is called from a closure built within the parse_data functionality near line 696 in tinyply.cpp

read = [this, &listSize, &dummyCount](PropertyLookup & f, const PlyProperty & p, uint8_t * dest, size_t & destOffset, std::istream & _is) noexcept
    {
        if (!p.isList)
        {
            read_property_ascii(p.propertyType, f.prop_stride, dest + destOffset, destOffset, _is);       [1]
        }
        else
        {
            read_property_ascii(p.listType, f.list_stride, &listSize, dummyCount, _is); // the list size
            for (size_t i = 0; i < listSize; ++i)
            {
                read_property_ascii(p.propertyType, f.prop_stride, dest + destOffset, destOffset, _is);   [2]
            }
        }
    }; 

Here we can see the vulnerability is triggerable both at [1] and [2] depending on if the property within the PLY file is a list or not. This closure is called later in parse_data, starting at line 725 in tinyply.cpp

std::vector<std::vector<PropertyLookup>> element_property_lookup = make_property_lookup_table();         [5]
size_t element_idx = 0;
size_t property_idx = 0;
ParsingHelper * helper {nullptr};

// This is the inner import loop
for (auto & element : elements)
{
    for (size_t count = 0; count < element.size; ++count)
    {
        property_idx = 0;
        for (auto & property : element.properties)
        {
            PropertyLookup & lookup = element_property_lookup[element_idx][property_idx];

            if (!lookup.skip)
            {
                helper = lookup.helper;                                                                   [4]
                if (firstPass)
                {
                    ...
                }
                else
                {
                    read(lookup, property, helper->data->buffer.get(), helper->cursor->byteOffset, is);   [3]
                }
            }
            else
            {
                skip(lookup, property, is);
            }
            property_idx++;
        }
    }
    element_idx++;
} 

At [3] the read closure is called with the buffer that is getting overwritten. This buffer populates the associated data at [4], starting from [5] within the make_property_lookup_table function.

IGL_INLINE std::vector<std::vector<PlyFile::PlyFileImpl::PropertyLookup>> PlyFile::PlyFileImpl::make_property_lookup_table()
{
    std::vector<std::vector<PropertyLookup>> element_property_lookup;

    for (auto & element : elements)
    {
        std::vector<PropertyLookup> lookups;

        for (auto & property : element.properties)
        {
            PropertyLookup f;

            auto cursorIt = userData.find(hash_fnv1a(element.name + property.name));
            if (cursorIt != userData.end()) f.helper = &cursorIt->second;                                [6]
            else f.skip = true;

            f.prop_stride = PropertyTable[property.propertyType].stride;
            if (property.isList) f.list_stride = PropertyTable[property.listType].stride;

            lookups.push_back(f);
        }

        element_property_lookup.push_back(lookups);
    }

    return element_property_lookup;
}

This function is responsible for building a lookup table of all of the properties of all of the elements that exist within the PLY file. At [6] specifically, we can see that the helper gets set to the value of the key:value pair in this userData dictionary. This userData entry is built from request_properties_from_element during the parsing of the file originally inreadPLY. The ReadPLY function has been condensed to make it easier to read.

IGL_INLINE bool readPLY(
  ...
  )
{
  tinyply::PlyFile file;
  file.parse_header(ply_stream);

  std::set<std::string> vertex_std{ "x","y","z", "nx","ny","nz",  "u","v",  "texture_u", "texture_v", "s", "t"};
  std::set<std::string> face_std  { "vertex_index", "vertex_indices"};
  std::set<std::string> edge_std  { "vertex1", "vertex2"}; //non-standard edge indexes                      [7]

  // Tinyply treats parsed data as untyped byte buffers.
  std::shared_ptr<tinyply::PlyData> vertices, normals, faces, texcoords, edges;

  ...

  std::shared_ptr<tinyply::PlyData> _edge_data;
  std::vector<std::string> _edge_header;

  for (auto c : file.get_comments())
      comments.push_back(c);

  for (auto e : file.get_elements())
  {
      ...
      else if(e.name == "edge" ) // found edge
      {
        for (auto p : e.properties)
        {
          if(edge_std.find(p.name) == edge_std.end())
          {
              _edge_header.push_back(p.name);                                                               [8]
          }
        }
      }
      // skip the unknown entries
  }

  ...


  try {
    edges = file.request_properties_from_element("edge", { "vertex1", "vertex2" });
  }
  catch (const std::exception & ) { }

  ...
  if(! _edge_header.empty())
    _edge_data = file.request_properties_from_element( "edge", _edge_header);                               [9]

  // Parse the geometry data
  file.read(ply_stream);                                                                                   [10]
  ...

This is the entrypoint into the code for reading PLY files using libIGL. At [7] you can see the expected property names that exist for the edge type. At [8] if the property name is not found in the default list, it is added to _edge_header, which is checked at [9] to determine if it is empty. If _edge_header is not empty, request_properties_from_element is called. At [10], read is called.

IGL_INLINE std::shared_ptr<PlyData> PlyFile::PlyFileImpl::request_properties_from_element(const std::string & elementKey,
const std::vector<std::string> propertyKeys,
const uint32_t list_size_hint)
{
if (elements.empty()) throw std::runtime_error("header had no elements defined. malformed file?");
if (elementKey.empty()) throw std::invalid_argument("`elementKey` argument is empty");
if (propertyKeys.empty()) throw std::invalid_argument("`propertyKeys` argument is empty");

std::shared_ptr<PlyData> out_data = std::make_shared<PlyData>();

const int64_t elementIndex = find_element(elementKey, elements);

std::vector<std::string> keys_not_found;

// Sanity check if the user requested element is in the pre-parsed header
if (elementIndex >= 0)
{
    // We found the element
    const PlyElement & element = elements[elementIndex];

    // Each key in `propertyKey` gets an entry into the userData map (keyed by a hash of
    // element name and property name), but groups of properties (requested from the
    // public api through this function) all share the same `ParsingHelper`. When it comes
    // time to .read(), we check the number of unique PlyData shared pointers
    // and allocate a single buffer that will be used by each property key group.
    // That way, properties like, {"x", "y", "z"} will all be put into the same buffer.

    ParsingHelper helper;                                                                                  [11]
    helper.data = out_data;
    helper.data->count = element.size; // how many items are in the element?
    helper.data->isList = false;
    helper.data->t = Type::INVALID;
    helper.cursor = std::make_shared<PlyDataCursor>();
    helper.list_size_hint = list_size_hint;

    // Find each of the keys
    for (const auto & key : propertyKeys)
    {
        const int64_t propertyIndex = find_property(key, element.properties);
        if (propertyIndex < 0) keys_not_found.push_back(key);
    }

    if (keys_not_found.size())
    {
        std::stringstream ss;
        for (auto & str : keys_not_found) ss << str << ", ";
        throw std::invalid_argument("the following property keys were not found in the header: " + ss.str());
    }

    for (const auto & key : propertyKeys)
    {
        const int64_t propertyIndex = find_property(key, element.properties);
        const PlyProperty & property = element.properties[propertyIndex];
        helper.data->t = property.propertyType;
        helper.data->isList = property.isList;
        auto result = userData.insert(std::pair<uint32_t, ParsingHelper>(hash_fnv1a(element.name + property.name), helper));    [12]
        if (result.second == false)
        {
            throw std::invalid_argument("element-property key has already been requested: " + element.name + " " + property.name);
        }
    }
.... 

At [11] a ParsingHelper is created and inserted into userData at [12] with the element name and property name, retrieved at [6]. The next step is moving into read, called from readPLY at [10].

IGL_INLINE void PlyFile::PlyFileImpl::read(std::istream & is)
{
std::vector<std::shared_ptr<PlyData>> buffers;
for (auto & entry : userData) buffers.push_back(entry.second.data);                                        [13]

// Discover if we can allocate up front without parsing the file twice
uint32_t list_hints = 0;
for (auto & b : buffers) for (auto & entry : userData) {list_hints += entry.second.list_size_hint;(void)b;}

// No list hints? Then we need to calculate how much memory to allocate
if (list_hints == 0)                                                                                       [14]
{
    parse_data(is, true);
}

// Count the number of properties (required for allocation)
// e.g. if we have properties x y and z requested, we ensure
// that their buffer points to the same PlyData
std::unordered_map<PlyData*, int32_t> unique_data_count;
for (auto & ptr : buffers) unique_data_count[ptr.get()] += 1;

// Since group-requested properties share the same cursor,
// we need to find unique cursors so we only allocate once
std::sort(buffers.begin(), buffers.end());
buffers.erase(std::unique(buffers.begin(), buffers.end()), buffers.end());

// We sorted by ptrs on PlyData, need to remap back onto its cursor in the userData table
for (auto & b : buffers)
{
    for (auto & entry : userData)
    {
        if (entry.second.data == b && b->buffer.get() == nullptr)
        {
            // If we didn't receive any list hints, it means we did two passes over the
            // file to compute the total length of all (potentially) variable-length lists
            if (list_hints == 0)
            {
                b->buffer = Buffer(entry.second.cursor->totalSizeBytes);                                   [15]
            }
            else
            {
            ...
            }

        }
    }
}

// Populate the data
parse_data(is, false);                                                                                     [16]
....

At [13] all of the buffers are collected from userData. These are currently initialized as null pointers in the constructor. At [14] a pre-pass on the data is done using parse_data (not triggering the vulnerability) because no list_hints are provided. This takes a different path through parse_data and will be discussed below. At [15] the buffer that is overflowed is allocated using the totalSizeBytes within cursor. This is set during the first pass of parse_data.

The first time parse_data is called, the function determines the size (as seen by the second argument being true). As such, a different closure is used skip and a firstPass path is taken through parse_data (this was clipped out near [4] when referenced earlier).

skip = [this, &listSize, &dummyCount, &skip_ascii_buffer](PropertyLookup & f, const PlyProperty & p, std::istream & _is) noexcept
    {
        skip_ascii_buffer.clear();
        if (p.isList)
        {
            read_property_ascii(p.listType, f.list_stride, &listSize, dummyCount, _is); // the list size (does not count for memory alloc)
            for (size_t i = 0; i < listSize; ++i) _is >> skip_ascii_buffer; // properties in list
            return listSize * f.prop_stride;
        }
        _is >> skip_ascii_buffer;
        return f.prop_stride;
    }; 

Below is the firstPass specific code, clipped from [4]. The only important aspect here is that skip is called and used to determine the totalSizeBytes, used at [15] in order to determine allocation size for the buffer.

if (firstPass)
    {
        helper->cursor->totalSizeBytes += skip(lookup, property, is);

        // These lines will be changed when tinyply supports
        // variable length lists. We add it here so our header data structure
        // contains enough info to write it back out again (e.g. transcoding).
        if (property.listCount == 0) property.listCount = listSize;
        if (property.listCount != listSize) throw std::runtime_error("variable length lists are not supported yet.");
    } 

The primary root of the issues resides in skip. This function uses listSize, which is initialized in parse_data as 0, to be passed into read_property_ascii. As discussed at the start of this report, this results in a ply_read_ascii call. If no data is provided, the call will fail without error; it is up to the user to check the istream using is.fail(). Without this check, the extraction from the istream will not modify any values. This leaves totalSizeBytes at 0, causing a buffer to be allocated with zero length. When parse_data is called a second time, read does not check that the buffer is large enough for the data being provided, and as such a heap buffer overflow occurs.

Crash Information

ASAN Report:
=================================================================
==2950937==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000050 at pc 0x5555556e218f bp 0x7fffffffd730 sp 0x7fffffffd728
WRITE of size 4 at 0x602000000050 thread T0
/usr/bin/addr2line: DWARF error: invalid or unhandled FORM value: 0x23
    #0 0x5555556e218e in igl::tinyply::PlyFile::PlyFileImpl::read_property_ascii(igl::tinyply::Type const&, unsigned long const&, void*, unsigned long&, std::istream&) ld-temp.o:?
    #1 0x5555556dbe7e in std::_Function_handler<void (igl::tinyply::PlyFile::PlyFileImpl::PropertyLookup&, igl::tinyply::PlyProperty const&, unsigned char*, unsigned long&, std::istream&), igl::tinyply::PlyFile::PlyFileImpl::parse_data(std::istream&, bool)::{lambda(igl::tinyply::PlyFile::PlyFileImpl::PropertyLookup&, igl::tinyply::PlyProperty const&, unsigned char*, unsigned long&, std::istream&)#2}>::_M_invoke(std::_Any_data const&, igl::tinyply::PlyFile::PlyFileImpl::PropertyLookup&, igl::tinyply::PlyProperty const&, unsigned char*&&, unsigned long&, std::istream&) ld-temp.o:?
    #2 0x5555556cf380 in igl::tinyply::PlyFile::PlyFileImpl::parse_data(std::istream&, bool) ld-temp.o:?
    #3 0x5555556a1a4d in igl::tinyply::PlyFile::PlyFileImpl::read(std::istream&) ld-temp.o:?
    #4 0x55555568e092 in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(std::istream&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&) ld-temp.o:?
    #5 0x555555684410 in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&) ld-temp.o:?
    #6 0x55555568370e in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&) ld-temp.o:?
    #7 0x555555682ff1 in main ??:?
    #8 0x7ffff7a3bd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58
    #9 0x7ffff7a3be3f in __libc_start_main_impl csu/../csu/libc-start.c:392
    #10 0x5555555b9014 in _start ??:?

0x602000000051 is located 0 bytes to the right of 1-byte region [0x602000000050,0x602000000051)
allocated by thread T0 here:
    #0 0x555555678dad in operator new[](unsigned long) ??:?
    #1 0x5555556a049b in igl::tinyply::PlyFile::PlyFileImpl::read(std::istream&) ld-temp.o:?
    #2 0x55555568e092 in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(std::istream&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&) ld-temp.o:?
    #3 0x555555684410 in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&) ld-temp.o:?
    #4 0x55555568370e in bool igl::readPLY<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> >&) ld-temp.o:?
    #5 0x555555682ff1 in main ??:?
    #6 0x7ffff7a3bd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow ld-temp.o:? in igl::tinyply::PlyFile::PlyFileImpl::read_property_ascii(igl::tinyply::Type const&, unsigned long const&, void*, unsigned long&, std::istream&)
Shadow bytes around the buggy address:
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa fd fa fa fa 00 00 fa fa[01]fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: 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
==2950937==ABORTING

Crash context:
Execution stopped here ==> 0x00007ffff7aa89fc: mov    r13d,eax

Register info:
   rax - 0x0000000000000000 (0)
   rbx - 0x00007ffff7955800 (140737347147776)
   rcx - 0x00007ffff7aa89fc (140737348536828)
   rdx - 0x0000000000000006 (6)
   rsi - 0x00000000002d0719 (2950937)
   rdi - 0x00000000002d0719 (2950937)
   rbp - 0x00000000002d0719 (0x2d0719)
   rsp - 0x00007fffffffc780 (0x7fffffffc780)
    r8 - 0x00007fffffffc850 (140737488341072)
    r9 - 0xbfffff00000fffff (-4611687117937967105)
   r10 - 0x0000000000000008 (8)
   r11 - 0x0000000000000246 (582)
   r12 - 0x0000000000000006 (6)
   r13 - 0x0000000000000016 (22)
   r14 - 0x1000000000000000 (1152921504606846976)
   r15 - 0x2000000000000000 (2305843009213693952)
   rip - 0x00007ffff7aa89fc (0x7ffff7aa89fc <__GI___pthread_kill+300>)
eflags - 0x00000246 ([ PF ZF IF ])
    cs - 0x00000033 (51)
    ss - 0x0000002b (43)
    ds - 0x00000000 (0)
    es - 0x00000000 (0)
    fs - 0x00000000 (0)
    gs - 0x00000000 (0)
TIMELINE

2023-11-22 - Initial Vendor Contact
2023-11-28 - Initial Vendor Contact
2023-11-30 - Request for confirmation
2023-12-11 - Advisories sent
2024-02-07 - Four more advisories sent, after the initial two
2024-02-27 - Request for status update
2024-04-10 - Request for status update
2ß24-05-15 - Request for status update via Github issue, no reply
2024-05-28 - Public Release

Credit

Discovered by Carl Hurd of Cisco Talos.