Skip to content

Bound out-of-range Object/Enum index lookups in BinaryAnnotator - #9212

Open
unpredictable21 wants to merge 1 commit into
google:masterfrom
unpredictable21:fix-binary-annotator-oob-index
Open

Bound out-of-range Object/Enum index lookups in BinaryAnnotator#9212
unpredictable21 wants to merge 1 commit into
google:masterfrom
unpredictable21:fix-binary-annotator-oob-index

Conversation

@unpredictable21

Copy link
Copy Markdown

Bound out-of-range Object/Enum index lookups in BinaryAnnotator

When flatc --annotate consumes a .bfbs schema, BinaryAnnotator walks
each Field's Type.index and dereferences
schema_->objects()->Get(field->type()->index()) (and the matching enums
vector) without first validating that the index is in range. The
reflection::VerifySchemaBuffer that runs before annotation only checks
structural integrity of the schema (offsets, sizes, alignment, vector
bounds); it never validates that a Field.type.index references a slot in
schema->objects() or schema->enums().

A schema with Type { base_type = Obj, index = N } where N >= schema->objects()->size() (and similarly for unions and enum-driven
indexes) reaches one of six unguarded sinks in binary_annotator.cpp /
binary_annotator.h. Each sink reads Vector::Get(N) where N is far
beyond the underlying heap allocation. In a release build without
FLATBUFFERS_ASSERT, the read proceeds past the heap buffer and into
adjacent heap pages; depending on the index value this either crosses into
unmapped memory (SEGV) or reads attacker-influenced heap contents.

The reproducer builds a minimal reflection Schema with one Object
whose single Field carries Type { base_type = Obj, index = 99999 }.
The schema passes reflection::VerifySchemaBuffer because every
structural constraint is satisfied. Running

flatc --annotate evil.bfbs evil.bfbs

on a debug build with assertions enabled trips
Vector::Get(i < size()); on a release build (or with assertions
compiled out under ASan) the read lands past the end of the
schema->objects() vector and produces

ERROR: AddressSanitizer: SEGV on unknown address 0x... (pc 0x...)
READ of size N at ...
    #0 ReadScalar<unsigned int>
    #1 IndirectHelper<Offset<reflection::Object>>::Read
    #2 Vector<Offset<reflection::Object>>::Get
    #3 BinaryAnnotator::IsInlineField
    #4 BinaryAnnotator::BuildTable
    #5 BinaryAnnotator::Annotate
    #6 FlatCompiler::AnnotateBinaries

Six sinks dereference the unchecked index:

File Line Sink
src/binary_annotator.h 394 IsInlineField (Obj base type, struct vs. table)
src/binary_annotator.cpp 757 BuildTable Obj case
src/binary_annotator.cpp 924 BuildStruct nested Obj
src/binary_annotator.cpp 980 BuildStruct array-of-struct Obj
src/binary_annotator.cpp 1153 BuildVector Obj element
src/binary_annotator.cpp 1432 BuildUnion enum lookup
src/binary_annotator.cpp 1445 BuildUnion Obj lookup after enum_val->union_type
src/binary_annotator.h 461 GetElementSize Obj element branch

This change adds two helpers, BinaryAnnotator::GetObject and
BinaryAnnotator::GetEnum, which return nullptr when the index is
negative or beyond the schema's objects() / enums() size. Every
unguarded sink is rewired through these helpers and bails out cleanly
when the index is out of bounds. The BuildStruct / BuildVector /
BuildUnion paths stop traversing when the bound is exceeded; the
BuildTable Obj path emits a generic (unknown) annotation for the
offending field so downstream regions remain well-formed.

The existing BinaryAnnotator::IsValidUnionValue already performs the
same kind of check for union value ids; the new GetObject / GetEnum
helpers extend the same pattern to all object/enum index dereferences.

Reproducer

poc/make_evil_bfbs.cpp emits the malicious schema. Build with
include/ on the include path:

g++ -std=c++17 -I include poc/make_evil_bfbs.cpp -o make_evil_bfbs
./make_evil_bfbs

The produced evil.bfbs validates with reflection::VerifySchemaBuffer
and crashes an unpatched flatc --annotate evil.bfbs evil.bfbs (debug
builds abort on the Vector::Get assert; release builds with ASan
report a SEGV in ReadScalar reached through
BinaryAnnotator::IsInlineField). Applying the diff in this change
makes the same invocation exit cleanly.

When flatc --annotate consumes a .bfbs schema, BinaryAnnotator walks
each Field's Type.index and dereferences
schema_->objects()->Get(field->type()->index()) (and the matching enums
vector) without first validating that the index is in range. The
reflection::VerifySchemaBuffer that runs before annotation only checks
structural integrity of the schema (offsets, sizes, alignment, vector
bounds); it never validates that a Field.type.index references a slot in
schema->objects() or schema->enums().

A schema with Type { base_type = Obj, index = N } where N >=
schema->objects()->size() (and similarly for unions and enum-driven
indexes) reaches one of eight unguarded sinks in binary_annotator.cpp /
binary_annotator.h. Each sink reads Vector::Get(N) where N is far
beyond the underlying heap allocation. In a release build without
FLATBUFFERS_ASSERT, the read proceeds past the heap buffer and into
adjacent heap pages; depending on the index value this either crosses
into unmapped memory (SEGV) or reads attacker-influenced heap contents.

This change adds two helpers, BinaryAnnotator::GetObject and
BinaryAnnotator::GetEnum, which return nullptr when the index is
negative or beyond the schema's objects() / enums() size. Every
unguarded sink is rewired through these helpers and bails out cleanly
when the index is out of bounds. The BuildStruct / BuildVector /
BuildUnion paths stop traversing when the bound is exceeded; the
BuildTable Obj path emits a generic (unknown) annotation for the
offending field so downstream regions remain well-formed.
@github-actions github-actions Bot added c++ codegen Involving generating code from schema labels Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ codegen Involving generating code from schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant