Talos Vulnerability Report

TALOS-2022-1593

Slic3r libslic3r TriangleMesh clone heap-based buffer overflow vulnerability

April 20, 2023
CVE Number

CVE-2022-36788

SUMMARY

A heap-based buffer overflow vulnerability exists in the TriangleMesh clone functionality of Slic3r libslic3r 1.3.0 and Master Commit b1a5500. A specially-crafted STL 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.

Slic3r libslic3r 1.3.0
Slic3r libslic3r Master Commit b1a5500

PRODUCT URLS

libslic3r - http://slic3r.org

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-130 - Improper Handling of Length Parameter Inconsistency

DETAILS

Slic3r is an open-source 3D printing toolbox, mainly utilized for translating 3D printing model file types into machine code for any printer. Slic3r uses libslic3er to do most of the non-GUI-based heavy lifting: reading various file formats, converting formats, and outputting appropriate gcode for the 3D printer’s given settings.

The libslic3r library, for parsing an STL file, will eventually execute the following function:

bool
STL::read(std::string input_file, TriangleMesh* mesh)
{
    try {
        mesh->ReadSTLFile(input_file);
        mesh->check_topology();
    } catch (...) {
        throw std::runtime_error("Error while reading STL file");
    }
    return true;
}

libslic3r heavily relies on ADMesh for several things, including STL manipulation. Indeed the ReadSTLFile function will eventually call ADMesh’s stl_open for parsing the STL file:

stl_open(stl_file *stl, const ADMESH_CHAR *file) {
  stl_initialize(stl);
  stl_count_facets(stl, file);
  stl_allocate(stl);
  stl_read(stl, 0, 1);
  if (!stl->error) fclose(stl->fp);
}

The function stl_count_facets will count how many triangles are present in the STL object provided. Then the stl_allocate will use that number to allocate the required number of facets:

void
stl_allocate(stl_file *stl) {
  [...]
  stl->facet_start = (stl_facet*)calloc(stl->stats.number_of_facets,
                                        sizeof(stl_facet));
  if(stl->facet_start == NULL) perror("stl_initialize");
  stl->stats.facets_malloced = stl->stats.number_of_facets;                                             [1]
[...]
}

The stl->stats.number_of_facets will be used to represent how many valid facets are in the stl->facet_start array. At [1], the number_of_facets is used to initialize the stl->stats.facets_malloced parameter. This parameter represents the actual number of elements allocated for the stl->facet_start array. At this point stl->stats.facets_malloced and stl->stats.number_of_facets coincide. Allegedly stl->stats.facets_malloced will always coincide with the actual allocated space for stl->facet_start.

The check_topology function eventually will remove the degenerate facets, the ones that do not form a valid triangle (for example, if two of the three vertices of any provided triangles are the same, they are removed).

libslic3r, during the read STL phase, will eventually reach the copy constructor for TriangleMesh:

TriangleMesh::TriangleMesh(const TriangleMesh &other)
    : stl(other.stl), repaired(other.repaired)
{
    this->clone(other);
}

This copy constructor will essentially copy 1-to-1 many parameters, including the stl->stats.facets_malloced parameter, and then call the clone function:

void TriangleMesh::clone(const TriangleMesh& other) {
    [..]
    if (other.stl.facet_start != NULL) {
        this->stl.facet_start = 
                  (stl_facet*)calloc(other.stl.stats.number_of_facets, sizeof(stl_facet));              [2]
        std::copy(
                  other.stl.facet_start, 
                  other.stl.facet_start + other.stl.stats.number_of_facets,
                  this->stl.facet_start);                                                               [3]
    }
    [..]
}

This function will manage the pointers of the new objects, creating new arrays and copying the contents from the other argument. For instance, at [2], the this->stl.facet_start, array that contains the actual facets, is allocated, and then, at [3] is populated with the original object contents.

After the whole copy constructor procedure stl->stats.facets_malloced could not coincide anymore with the actual allocated space for stl->facet_start . Indeed, stats.facets_malloced is copied as it is, but the stats.number_of_facets can be less than the allocated spaces (for instance, for the removed degenerate facets). After the copy constructor, the object could have a stl.facet_start that is allocated with a smaller number than stats.facets_malloced. Because these variable are used in the ADMesh’s functions to understand if it is required to allocate more space for the stl.facet_start array, this problem can lead to a heap buffer overflow.

For example, the function used to add a facet is ADMesh’s stl_add_facet:

void
stl_add_facet(stl_file *stl, stl_facet *new_facet) {
  if (stl->error) return;

  stl->stats.facets_added += 1;
  if(stl->stats.facets_malloced < stl->stats.number_of_facets + 1) {                                    [4]
    [... Allocate more space ...]
  }
  stl->facet_start[stl->stats.number_of_facets] = *new_facet;                                           [5]

  [...]
}

We can analyze at [4] the stl object, loading a specific STL, to realize that a buffer overflow could occur:

(gdb) p *stl
$5 = {
  [..]
  facet_start = 0x9b5c70,
  [...]
  stats = {

    number_of_facets = 0x3,
    [..]
    facets_malloced = 0x4,
    [...]
  },
[...]
}

Above it is shown that the stl object has stats.number_of_facets=3 and stats.facets_malloced=4.

(gdb) heap chunk stl->facet_start
Chunk(addr=0x9b5c70, size=0xb0, flags=PREV_INUSE)
Chunk size: 176 (0xb0)
Usable size: 168 (0xa8)
[...]

The facet_start has size 0xa8. So, the number of elements in the array is:

(gdb) p 0xa8/sizeof(stl_facet)
$15 = 0x3

With the example provided, at [4], the branch will not be taken and so no reallocation will take place. This will cause a heap buffer overflow at [5].

Crash Information

==8==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60e00000133c at pc 0x556e0e8d47c6 bp 0x7ffc72e7a6a0 sp 0x7ffc72e7a698
WRITE of size 52 at 0x60e00000133c thread T0
    #0 0x556e0e8d47c5 in stl_add_facet /app/Slic3r/xs/src/admesh/connect.c:977
    #1 0x556e0e8d3fd3 in stl_fill_holes /app/Slic3r/xs/src/admesh/connect.c:935
    #2 0x556e0e8eb2a9 in stl_repair /app/Slic3r/xs/src/admesh/util.c:589
    #3 0x556e0e621c63 in Slic3r::TriangleMesh::repair() /app/Slic3r/xs/src/libslic3r/TriangleMesh.cpp:186
    #4 0x556e0e59229e in Slic3r::ModelObject::repair() /app/Slic3r/xs/src/libslic3r/Model.cpp:602
    #5 0x556e0e58b522 in Slic3r::Model::repair() /app/Slic3r/xs/src/libslic3r/Model.cpp:211
    #6 0x556e0e505745 in main /app/Slic3r/src/slic3r.cpp:20
    #7 0x7f6f0cbee81c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2381c)
    #8 0x556e0e5054f9 in _start (/app/Slic3r/build/slic3r+0x6f4f9)

0x60e00000133c is located 0 bytes to the right of 156-byte region [0x60e0000012a0,0x60e00000133c)
allocated by thread T0 here:
    #0 0x7f6f0d1f0987 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x556e0e621339 in Slic3r::TriangleMesh::clone(Slic3r::TriangleMesh const&) /app/Slic3r/xs/src/libslic3r/TriangleMesh.cpp:103
    #2 0x556e0e621035 in Slic3r::TriangleMesh::TriangleMesh(Slic3r::TriangleMesh const&) /app/Slic3r/xs/src/libslic3r/TriangleMesh.cpp:85
    #3 0x556e0e59ae10 in Slic3r::ModelVolume::ModelVolume(Slic3r::ModelObject*, Slic3r::TriangleMesh const&) /app/Slic3r/xs/src/libslic3r/Model.cpp:1054
    #4 0x556e0e590ce5 in Slic3r::ModelObject::add_volume(Slic3r::TriangleMesh const&) /app/Slic3r/xs/src/libslic3r/Model.cpp:499
    #5 0x556e0e524c80 in Slic3r::IO::STL::read(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, Slic3r::Model*) /app/Slic3r/xs/src/libslic3r/IO.cpp:59
    #6 0x556e0e5056ef in main /app/Slic3r/src/slic3r.cpp:19
    #7 0x7f6f0cbee81c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2381c)

SUMMARY: AddressSanitizer: heap-buffer-overflow /app/Slic3r/xs/src/admesh/connect.c:977 in stl_add_facet
Shadow bytes around the buggy address:
  0x0c1c7fff8210: fd fd fd fd fa fa fa fa fa fa fa fa 00 00 00 00
  0x0c1c7fff8220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c1c7fff8230: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x0c1c7fff8240: 00 00 00 00 00 00 00 00 00 00 00 00 fa fa fa fa
  0x0c1c7fff8250: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c1c7fff8260: 00 00 00 00 00 00 00[04]fa fa fa fa fa fa fa fa
  0x0c1c7fff8270: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c1c7fff8280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c1c7fff8290: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c1c7fff82a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c1c7fff82b0: 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
  Shadow gap:              cc
==8==ABORTING
TIMELINE

2022-09-01 - Vendor Disclosure
2023-04-03 - Last Contact Date
2023-04-20 - Public Release

Credit

Discovered by Francesco Benvenuto of Cisco Talos.