Skip to content

Commit 3f34c39

Browse files
FlexBuffers: reject cyclic buffers in the C++ Verifier
The FlexBuffers verifier accepted cyclic buffers when a reuse_tracker was supplied. Because offsets point backwards, a vector/map element can reference an ancestor node that is still being verified; the reuse tracker's de-duplication treated that re-visit as "already verified" and returned true, so verification passed. Recursive accessors (Reference::ToString() and any user traversal) then followed the cycle with no depth/cycle guard, recursing until the stack was exhausted (DoS). Without a reuse_tracker the depth limit already rejected these buffers; the tracker's early return is what bypassed it. Make the reuse tracking cycle-aware in VerifyVector: mark a node in-progress while its children are verified and mark it verified on completion. A re-visit of an in-progress node is a cycle (reject); a re-visit of a completed node with the same type is a legitimate shared/DAG reference (skip, as before). This preserves DAG de-duplication of shared keys/strings and keeps verification linear. Also add a regression test (FlexBuffersCyclicBufferTest) and extend flexbuffers_verifier_fuzzer to access the buffer after a successful verify, so the "verified => safe to access" property is fuzzed going forward.
1 parent 81edeb1 commit 3f34c39

5 files changed

Lines changed: 45 additions & 3 deletions

File tree

include/flatbuffers/flexbuffers.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,8 +1930,25 @@ class Verifier FLATBUFFERS_FINAL_CLASS {
19301930
return false;
19311931
auto size_byte_width = r.byte_width_;
19321932
if (!VerifyBeforePointer(p, size_byte_width)) return false;
1933-
FLEX_CHECK_VERIFIED(p - size_byte_width,
1934-
PackedType(Builder::WidthB(size_byte_width), r.type_));
1933+
// Cycle-aware reuse tracking. A node that is still being verified (i.e.
1934+
// present on the current verification path) is a cycle and must be
1935+
// rejected; a fully-verified node with the same type is a legitimate
1936+
// shared (DAG) reference and can be skipped. Distinguishing the two
1937+
// prevents cyclic buffers from passing verification -- which otherwise
1938+
// caused unbounded recursion / stack exhaustion in accessors such as
1939+
// Reference::ToString() -- while preserving the DAG de-duplication the
1940+
// reuse tracker exists for.
1941+
const uint8_t kInProgressMarker = 0xFF; // not a valid PackedType value.
1942+
const auto vpacked = PackedType(Builder::WidthB(size_byte_width), r.type_);
1943+
const size_t vpos = static_cast<size_t>((p - size_byte_width) - buf_);
1944+
if (reuse_tracker_) {
1945+
FLATBUFFERS_ASSERT(vpos < reuse_tracker_->size());
1946+
auto existing = (*reuse_tracker_)[vpos];
1947+
if (existing == kInProgressMarker) return false; // cycle detected.
1948+
if (existing == vpacked) return true; // shared (DAG) node.
1949+
if (!Check(existing == 0)) return false; // type mismatch.
1950+
(*reuse_tracker_)[vpos] = kInProgressMarker;
1951+
}
19351952
auto sized = Sized(p, size_byte_width);
19361953
auto num_elems = sized.size();
19371954
auto elem_byte_width = r.type_ == FBT_STRING || r.type_ == FBT_BLOB
@@ -1955,6 +1972,7 @@ class Verifier FLATBUFFERS_FINAL_CLASS {
19551972
} else {
19561973
FLATBUFFERS_ASSERT(IsInline(elem_type));
19571974
}
1975+
if (reuse_tracker_) (*reuse_tracker_)[vpos] = vpacked; // fully verified.
19581976
depth_--;
19591977
return true;
19601978
}

tests/flexbuffers_test.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,21 @@ void FlexBuffersReuseBugTest() {
187187
true);
188188
}
189189

190+
void FlexBuffersCyclicBufferTest() {
191+
// Cyclic buffers must be rejected by the verifier. Before the cycle-aware
192+
// reuse-tracking fix, these buffers passed VerifyBuffer() when a reuse
193+
// tracker was supplied, and then caused unbounded recursion / stack
194+
// exhaustion in recursive accessors such as Reference::ToString().
195+
// A vector whose only element is the vector itself (self-loop).
196+
const uint8_t self_loop[] = { 0x01, 0x00, 0x28, 0x01 };
197+
// A vector element that points back to the parent vector (parent cycle).
198+
const uint8_t parent_cycle[] = { 0x02, 0x24, 0x01, 0x02, 0x28, 0x01 };
199+
std::vector<uint8_t> t1, t2;
200+
TEST_EQ(flexbuffers::VerifyBuffer(self_loop, sizeof(self_loop), &t1), false);
201+
TEST_EQ(flexbuffers::VerifyBuffer(parent_cycle, sizeof(parent_cycle), &t2),
202+
false);
203+
}
204+
190205
void FlexBuffersFloatingPointTest() {
191206
#if defined(FLATBUFFERS_HAS_NEW_STRTOD) && (FLATBUFFERS_HAS_NEW_STRTOD > 0)
192207
flexbuffers::Builder slb(512,

tests/flexbuffers_test.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace tests {
66

77
void FlexBuffersTest();
88
void FlexBuffersReuseBugTest();
9+
void FlexBuffersCyclicBufferTest();
910
void FlexBuffersFloatingPointTest();
1011
void FlexBuffersDeprecatedTest();
1112
void ParseFlexbuffersFromJsonWithNullTest();

tests/fuzzer/flexbuffers_verifier_fuzzer.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
1212
std::vector<uint8_t> reuse_tracker;
1313
// Check both with and without reuse tracker paths.
14-
flexbuffers::VerifyBuffer(data, size, &reuse_tracker);
14+
if (flexbuffers::VerifyBuffer(data, size, &reuse_tracker)) {
15+
// A buffer that passes verification must be safe to fully traverse. This
16+
// exercises the "verified => safe to access" contract that the verify-only
17+
// path above does not (previously untested), and would catch e.g. cyclic
18+
// buffers that recurse without bound.
19+
std::string s;
20+
flexbuffers::GetRoot(data, size).ToString(true, true, s);
21+
}
1522
// FIXME: we can't really verify this path, because the fuzzer will
1623
// construct buffers that time out.
1724
// Add a simple #define to bound the number of steps just for the fuzzer?

tests/test.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,7 @@ int FlatBufferTests(const std::string& tests_data_path) {
18291829
CreateSharedStringTest();
18301830
FlexBuffersTest();
18311831
FlexBuffersReuseBugTest();
1832+
FlexBuffersCyclicBufferTest();
18321833
FlexBuffersDeprecatedTest();
18331834
UninitializedVectorTest();
18341835
EqualOperatorTest();

0 commit comments

Comments
 (0)