Skip to content

FlexBuffers: reject cyclic buffers in the C++ Verifier (fixes stack-exhaustion DoS on access)#9187

Open
asroyxCySec wants to merge 1 commit into
google:masterfrom
asroyxCySec:fix/flexbuffers-verifier-cycle
Open

FlexBuffers: reject cyclic buffers in the C++ Verifier (fixes stack-exhaustion DoS on access)#9187
asroyxCySec wants to merge 1 commit into
google:masterfrom
asroyxCySec:fix/flexbuffers-verifier-cycle

Conversation

@asroyxCySec

Copy link
Copy Markdown

Problem

flexbuffers::VerifyBuffer(buf, len, &reuse_tracker) accepts cyclic buffers. Because FlexBuffers offsets point backwards, a value inside a vector/map can reference an ancestor node that is still being verified. The reuse tracker's de-duplication treats that re-visit as "already verified" and returns true, so verification succeeds. The recursive accessors (Reference::ToString(), and any user code that walks the structure) then follow the cycle with no depth/cycle guard and recurse until the stack is exhausted → crash.

This breaks the guarantee that a buffer passing VerifyBuffer is safe to access.

Minimal reproducers (both pass verification with a reuse tracker before this change, then crash on access; both are correctly rejected after it):

  • 01 00 28 01 (4 bytes) — a vector whose element is the vector itself (self-loop).
  • 02 24 01 02 28 01 (6 bytes) — a vector element that points back to the parent vector.
const uint8_t buf[] = {0x01, 0x00, 0x28, 0x01};
std::vector<uint8_t> tracker;
// TRUE before this change:
flexbuffers::VerifyBuffer(buf, sizeof buf, &tracker);
std::string s;
flexbuffers::GetRoot(buf, sizeof buf).ToString(true, true, s); // stack overflow before this change

Note: without a reuse tracker the depth limit already rejects these buffers; the tracker's early "already verified" return is what bypasses that limit.

Fix

Make the reuse tracking cycle-aware in Verifier::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 the tracker's DAG de-duplication (shared keys/strings/key-vectors) and keeps verification linear.

Testing

  • The official test suite passes (flattests, built with -fsanitize=address): ALL TESTS PASSED, including FlexBuffersTest, FlexBuffersReuseBugTest, and the new FlexBuffersCyclicBufferTest. No regressions.
  • Added FlexBuffersCyclicBufferTest asserting both cyclic reproducers are rejected.
  • Extended tests/fuzzer/flexbuffers_verifier_fuzzer.cc to call GetRoot(...).ToString(...) after a successful verify, so the "verified ⇒ safe to access" property is fuzzed (it previously only verified, never accessed).
  • Fuzzing: the differential harness produced 400+ stack-exhaustion crashes on the unpatched build within seconds; the patched build ran ~5,000,000 executions with 0 crashes/timeouts/OOM.

@google-cla

google-cla Bot commented Jul 26, 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.

@github-actions github-actions Bot added the c++ label Jul 26, 2026
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.
@asroyxCySec
asroyxCySec force-pushed the fix/flexbuffers-verifier-cycle branch from c236f8f to 3f34c39 Compare July 26, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants