Skip to content

Fix heap OOB read in reflection-based Verify() from unchecked Type.index() - #9198

Open
prasanna8585 wants to merge 2 commits into
google:masterfrom
prasanna8585:fix/reflection-verify-oob-index
Open

Fix heap OOB read in reflection-based Verify() from unchecked Type.index()#9198
prasanna8585 wants to merge 2 commits into
google:masterfrom
prasanna8585:fix/reflection-verify-oob-index

Conversation

@prasanna8585

Copy link
Copy Markdown

Summary

VerifyUnion, VerifyVector, and VerifyObject in reflection.cpp use field.type()->index() to index into schema.objects()/schema.enums() without validating the index is in range. Vector::Get()'s only bounds check is an assert that is compiled out in NDEBUG (release) builds, so a schema with an out-of-range Type.index() causes an out-of-bounds read when verified via the reflection API.

Who is affected

Applications that call flatbuffers::Verify(schema, root, buf, len) with a schema from an untrusted or externally supplied source (e.g. tools that load .bfbs schemas at runtime, per the documented reflection use case).

Fix

Adds a range check (0 <= index() < size()) at each of the three call sites in the verification path before calling Get(), returning false (verification failure) on an out-of-range index rather than reading out of bounds.

Testing

  • Added ReflectionInvalidObjectIndexTest to tests/reflection_test.cpp, which builds a schema with an empty objects vector and a field type.index() far out of range, and confirms Verify() now fails closed.
  • Confirmed via local build that this test aborts (hits the Vector::Get assert) against the unfixed source, and passes cleanly with the fix applied.
  • Full existing test suite (flattests) passes with no regressions.

Note for maintainers

CopyTable/CopyInline (roughly lines 680-757) have the same unchecked index() pattern, but they operate on data that callers are expected to have already run through Verify(). Left out of this fix to keep the change minimal and focused on the verification path itself; flagging here in case it's worth a follow-up.

…dex()

VerifyUnion, VerifyVector, and VerifyObject in reflection.cpp use
field.type()->index() to index into schema.objects()/schema.enums()
without validating the index is in range. Vector::Get()'s only bounds
check is an assert that is compiled out in NDEBUG (release) builds, so
a schema with an out-of-range Type.index() causes an out-of-bounds read
when verified via the reflection API. This affects callers using
flatbuffers::Verify() with a schema from an untrusted or externally
supplied source.

Adds bounds checks at all three affected call sites so verification
fails closed instead, plus a regression test that builds a schema with
an empty objects vector and an out-of-range field type.index(), and
confirms Verify() now returns false instead of reading out of bounds.

Note: CopyTable/CopyInline (~L680-757) have the same unchecked index()
pattern but operate on data expected to already be verified; leaving
those out of this fix to keep it minimal and reviewable, flagging for
a maintainer look separately.
@github-actions github-actions Bot added c++ codegen Involving generating code from schema labels Aug 12, 2026
@prasanna8585

Copy link
Copy Markdown
Author

Hi @dbaileychess,
Just following up to check if any additional changes or steps are needed on my end, or if this is ready to be merged.

…nchecked Type.index()

Independent finding, found by investigating a lead my own prior fix
(heap OOB read in reflection-based Verify(), VerifyUnion/VerifyVector/
VerifyObject) explicitly left open in its own commit message:
CopyTable/CopyInline have the same unchecked index() pattern but were
assumed to 'operate on data expected to already be verified.' That
assumption was never actually verified.

Neither CopyTable nor ResizeContext::ResizeTable (reached via the
public SetString/ResizeAnyVector mutation APIs) requires the caller
to have verified the schema against the data -- and even a caller
that did verify may have done so against a different, benign schema
than the one passed here. An out-of-range Type.index() reaches
Vector::Get(), whose only bounds check is an assert compiled out in
release (NDEBUG) builds.

Found and fixed 5 instances of the identical pattern:
  - 3 sites in CopyTable (the top-level Obj field case, the
    vector-of-Obj element case, and the second loop that builds the
    actual output table)
  - 2 sites in ResizeContext::ResizeTable (the direct Obj field case
    and the vector-of-Obj element case)

Dynamically confirmed both as real, independent crashes with real
ASan runs: a malicious schema (empty objects vector, a field with
Type.index()=999999) passed to CopyTable produced a genuine
ASan-reported SEGV from an out-of-bounds read; the same schema shape,
reached via SetString (whose resize triggers ResizeTable's
whole-object-graph walk as a side effect of resizing an unrelated
string field), produced a second, independent ASan-reported SEGV
inside ResizeTable.

Fix: CopyTable's sites skip the malformed field entirely, mirroring
how the same function already tolerates an absent field. The second
loop specifically must  rather than  on an
out-of-range index, since  would fall through to code
expecting to consume an entry from the offsets vector that was never
pushed for this field, desynchronizing every field processed after
it. ResizeContext::ResizeTable's sites are simpler: the existing
downstream code already null-safely handles a missing object
definition, so resolving it safely (nullptr on out-of-range) is
sufficient on its own.

Dedup checked: fetched the one existing, superficially-similar public
report (google#9040, Heap Buffer Overflow in FlatBuffers Reflection
Verifier) in full and confirmed it is a genuinely different bug --
its root cause is GetFieldT reading a table field offset without
checking it against the table's own size, not an out-of-range index
into schema.objects(). Searched this repo's history (git log -S on
the exact vulnerable line) and found no prior fix attempt.

Verified: both PoCs re-run against the patched code now complete
cleanly with no crash. Added 2 new regression tests
(CopyTableInvalidObjectIndexTest, ResizeTableInvalidObjectIndexTest),
matching the file's own existing ReflectionInvalidObjectIndexTest
convention. Built the full existing test suite with ASan and ran it
end to end: ALL TESTS PASSED, including both new tests, with no
regressions.
@prasanna8585

Copy link
Copy Markdown
Author

Hi @dbaileychess,
Just following up to check if any additional changes or steps are needed on my end, or if this is ready to be merged.

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