Skip to content

[C++] Object API: Pack() dereferences null when a union has its type set but no value (buffer passes the verifier) #9416

Open

Description

@professor-moody

A table with a union field whose _type is set but whose value is absent passes Verify...Buffer (VerifyTable accepts a null table, include/flatbuffers/verifier.h:119-121). The generated UnPackTo copies the type and skips the value, which leaves test.type = Any::Monster and test.value = nullptr in the native object. The generated union Pack then casts value and calls Create<T>(_fbb, ptr, ...) without a null check, and the callee reads through the null object.

Minimal repro using only the public builder, tests/monster_test.fbs, and code generated by flatc 25.12.19 at master b8431fbc (flatc --cpp --gen-object-api --gen-mutable --scoped-enums):

flatbuffers::FlatBufferBuilder fbb;
auto name = fbb.CreateString("m");
MonsterBuilder mb(fbb);
mb.add_name(name);
mb.add_test_type(Any::Monster);          // type set, no value
FinishMonsterBuffer(fbb, mb.Finish());
flatbuffers::Verifier v(fbb.GetBufferPointer(), fbb.GetSize());
bool ok = VerifyMonsterBuffer(v);        // true
std::unique_ptr<MonsterT> u(GetMonster(fbb.GetBufferPointer())->UnPack());
// u->test.type == Any::Monster, u->test.value == nullptr
flatbuffers::FlatBufferBuilder f2;
f2.Finish(Monster::Pack(f2, u.get()));   // crashes

Observed on Linux x86_64 with clang 21.1.8:

without add_test_type:  verify=1  unpacked test.type=0 value=(nil)  repacked size=144
with add_test_type:     verify=1  unpacked test.type=1 value=(nil)
  -fsanitize=address,undefined: monster_test_generated.h:2832:19: runtime error: member access within null pointer of type 'const MonsterT'
                                  in Monster::Pack (:2832) <- Monster::Pack (:2839, auto _test = _o->test.Pack(_fbb))
  -O3 -DNDEBUG: the process stops before "repacked"

A fuzzer that runs VerifyMonsterBuffer (max_depth 16, max_tables 1024, check_alignment, check_nested_flatbuffers) followed by UnPack() and Pack() hit the same pattern through several unions in the test schema (Any, AnyAmbiguousAliases, AnyUniqueAliases, a testarrayoftables element, and enemy -> any_unique). In a build without sanitizers it ends in a SEGV on a read of address 0, for example at TestSimpleTableWithEnum::Pack (auto _color = _o->color;, monster_test_generated.h:2560) via AnyUnion::Pack (:3080). The smallest input is 56 bytes:

240000004d4f4e531a000c0000000000000004004000040003000c0000000000080000001c00000008000000080000000200000000000000

The emitting code is the union Pack template in src/idl_gen_cpp.cpp:1752-1783 (identical at v25.12.19). Two possible fixes: emit if (!value) return 0; at the top of the generated union Pack, which preserves the input ("type set, value absent") on round-trip, or reset the type to NONE in the generated UnPackTo when the value is absent. I have not prepared a PR; I am happy to do one for whichever you prefer, with regenerated code and a test.

Related, separate: #9033 / #9308 fix the same "type without value" shape in the JSON printer, which is a different consumer.

A second, smaller Object API issue showed up in the same run. The generated 64-bit RootTable::Pack (tests/64bit/test_64bit.fbs) creates near_string (a 32-bit offset) before ForceVectorAlignment64 / CreateVector64. For a RootTableT with only near_string = "x", a debug build hits FLATBUFFERS_ASSERT(GetSize() == length_of_64_bit_region_) (flatbuffer_builder.h:724), and an -O3 -DNDEBUG build returns a 36-byte buffer for which VerifyRootTableBuffer returns false. An empty RootTableT packs to 12 bytes and verifies. I can open that as its own issue if you prefer.

AI assistance: found by a fuzzing pipeline; report prepared with AI assistance under the reporter's direction and review; every result was executed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions