Skip to content

Fix type confusion and out-of-bounds read in reflection union verification - #9220

Open
Grolar1337 wants to merge 1 commit into
google:masterfrom
Grolar1337:fix/reflection-union-verify
Open

Fix type confusion and out-of-bounds read in reflection union verification#9220
Grolar1337 wants to merge 1 commit into
google:masterfrom
Grolar1337:fix/reflection-union-verify

Conversation

@Grolar1337

@Grolar1337 Grolar1337 commented Sep 4, 2026

Copy link
Copy Markdown

Fixes a reflection-verifier defect found during a security review of
attacker-supplied reflection schemas (.bfbs) and data buffers consumed
through flatbuffers::reflection::Verify.

Branch: fix/reflection-union-verify (from master @ 5761d6e).
Commit: ca38ac5f — "Fix type confusion and OOB read in reflection union verification"

Problem

VerifyUnion (src/reflection.cpp) resolved a union's concrete member type
by using the wire discriminator as a position into the union enum's
values() vector, instead of matching it against declared member values:

auto elem_type = fb_enum->values()->Get(utype)->union_type();  // index != value

Consequences:

  1. Type confusion / verifier bypass. A tag that is a valid values()
    position but not a declared value (e.g. 1 for union ABC : int { A = 555, B = 666 }) was verified against the wrong member. A B (string)
    payload tagged 1 was accepted as member A.
  2. False reject of legitimate buffers. The discriminator is stored at the
    union enum's underlying width (int32 for the example above), but was read
    as a hard-coded uint8_t, so a legitimate tag == 555 was read as byte
    43 and rejected.
  3. Out-of-bounds read. VerifyUnion dereferenced Type.index with an
    unchecked schema.objects()->Get(elem_type->index()).
    reflection::VerifySchemaBuffer performs structural checks only — it never
    validates that a Type.index is in range against schema.objects() /
    schema.enums(). A schema carrying an out-of-range union-member index
    verifies cleanly and then drives an out-of-bounds read in VerifyUnion.
  4. Consumer null read. The reflected consumer GetUnionType did a
    values()->LookupByKey(tag) and dereferenced the result; for a tag that is
    not a declared value this is a null read on a buffer the verifier accepted.

Changes

src/reflection.cpp

  • VerifyUnion: resolve the member by value — iterate the enum's
    values() and match EnumVal.value() to the discriminator, exactly like
    the generated per-schema union verifiers (switch over real values). No
    matching member value ⇒ reject. Bounds-check Type.index against
    objects().size() before the objects()->Get(...) dereference.
  • Discriminator width: read the discriminator at the union enum's
    underlying width (underlying_type()->base_size(): 1/2/4/8 bytes) instead
    of a hard-coded uint8_t, in VerifyObject's union-field handling and in
    the union-vector path. Legitimate sparse unions (A = 555, ...) now
    verify; undeclared tags are rejected.
  • GetSchemaObject / GetSchemaEnum: bounds-checked accessors used by
    the verifier's objects()->Get(...) / enums()->Get(...) sites
    (VerifyVector Obj case, VerifyObject Obj case, VerifyUnion).
  • SchemaIsValid: new cross-reference validator — rejects any schema
    whose Type.index (for Obj/Union/Vector-of-Obj/Union types) is out of range
    against objects() / enums(). Called at the entry of
    reflection::Verify / VerifySizePrefixed.
  • GetAnyValueS: guard the objects()->Get(type_index) debug-print path.

include/flatbuffers/reflection.h

  • SchemaIsValid declaration.
  • GetUnionType: new null-safe overload returning bool with an out
    parameter. It validates the enum index, reads the discriminator at the
    enum's underlying width, matches by value, and bounds-checks the object
    index before dereferencing. The legacy reference-returning overload is kept
    for source compatibility (only safe on already-verified buffers).
  • CopyTable / ResizeTable call sites updated to the null-safe overload;
    unresolvable union members are skipped instead of crashing.

src/binary_annotator.{h,cpp}

  • BinaryAnnotator::Annotate() runs SchemaIsValid before walking the schema
    (both constructors).
  • IsValidUnionValue matches the discriminator by value (not position).
  • IsInlineField, GetElementSize, and BuildUnion bounds-check
    Type.index before dereferencing and handle unresolvable unions gracefully.

Tests

tests/reflection_union_security_test.cpp, registered as
ReflectionUnionSecurityTest() in tests/test.cpp and added to
FlatBuffers_Tests_SRCS in CMakeLists.txt. It exercises:

  • SparseUnionTests: legitimate buffers with tag = 555 / 666 (stored
    int32) now verify; a hand-crafted tag 1 (a position but not a declared
    value) is rejected.
  • OutOfRangeIndexTests: a schema whose union-member Type.index is out of
    range is rejected by reflection::Verify (via SchemaIsValid) instead of
    performing an out-of-bounds read.
  • ConsumerNullSafetyTests: CopyTable on an unresolvable-union buffer no
    longer crashes.

Verification

Regression suite (ReflectionUnionSecurityTest) and the full flattests run
were built and executed locally (Debug + AddressSanitizer); expected output
includes:

SparseUnionTests
OutOfRangeIndexTests
ConsumerNullSafetyTests
ReflectionUnionSecurityTest: PASSED

Please re-run locally if needed:

cmake -S . -B build -DFLATBUFFERS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build --target flattests -j$(nproc)
./build/flattests

Relationship to other PRs

Independent of harden/deserialize-input-contract, which hardens
Parser::Deserialize input handling and Type::Deserialize range validation.
The two branches share only src/reflection.cpp / tests/test.cpp /
CMakeLists.txt at the file level; they fix distinct root causes and should
rebase cleanly.

PR body notes for maintainers

  • Signed-off-by present on the commit (CLA signed).
  • Security fix: reflection verifier type confusion + OOB read on
    attacker-supplied .bfbs schemas.

VerifyUnion treated the union discriminator as a vector position
instead of matching the declared enum value, and read it at a fixed
8-bit width even for int32-backed unions; the resolved member's
Type.index reached objects()->Get() with no bounds check, and no
cross-validation of these indices against schema sizes.

- VerifyUnion/VerifyObject: value-based member resolution,
  declared-width discriminator read, no-match rejected
- SchemaIsValid(): cross-reference validation at Verify/Annotate entry
- Bounds guards on all objects()/enums()->Get sites in
  reflection.cpp and binary_annotator
- GetUnionType: null-safe bool overload (legacy overload preserved)
- Regression test: reflection_union_security_test.cpp

Signed-off-by: Grolar1337 <blkyakupsait@gmail.com>
@github-actions github-actions Bot added c++ codegen Involving generating code from schema labels Sep 4, 2026
@google-cla

google-cla Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@Grolar1337

Copy link
Copy Markdown
Author

I just submitted the CLA form, is that correct?

@Grolar1337 Grolar1337 changed the title Fix type confusion and OOB read in reflection union verification Fix type confusion and out-of-bounds read in reflection union verification Sep 4, 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