Repository navigation
[C++] Object API: Pack() dereferences null when a union has its type set but no value (buffer passes the verifier) #9416
Description
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())); // crashesObserved 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