Repository navigation
flexbuffers: cap Reference::ToString recursion depth on cyclic and nested buffers - #9454
Open
itsgaldoron wants to merge 1 commit into
Open
itsgaldoron wants to merge 1 commit into
itsgaldoron wants to merge 1 commit into
Conversation
…sted buffers Reference::ToString() and AppendToString<T>() call each other once per container level with no depth bound (flexbuffers.h:664 -> AppendToString, :387 -> child ToString, :652 -> map value ToString), so a cyclic or deeply nested FlexBuffer exhausts the stack. A 6-byte buffer whose single-element vector points at itself passes VerifyBuffer whenever a reuse tracker is supplied (the flatc --flexbuffers --json path) and then crashes the printer: 248 alternating ToString/AppendToString frames until the stack guard. Cap the recursion at kToStringMaxDepth = 64 and truncate with "...". The cap lives in ToString() itself (not AppendToString) so the map branch is covered by the same check; cur_indent already tracks container depth, so no new parameter is needed. Add FlexBuffersToStringRecursionTest(): a benign nested buffer renders byte-identical (control), and the cyclic and 100-deep probes run in a forked child so an uncapped build fails the test by crashing the child instead of the test runner.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reference::ToString()andAppendToString<T>()recurse into each other once per container level with no depth bound. The vector branches ofToStringcallAppendToString<Vector/TypedVector/FixedTypedVector>(include/flatbuffers/flexbuffers.h:664), which calls back intochild.ToString(...)at:387, and the map branch recurses throughvals[i].ToString(...)at:652.cur_indentis already threaded through all of these and increments once per level, but nothing ever checks it.A 6-byte FlexBuffer is enough to blow the stack — a one-element vector whose single element points at the vector itself:
VerifyBuffer()accepts this whenever a reuse tracker is supplied: the tracker marks the vector's size byte as verified on the first visit, and the self-reference hits theFLEX_CHECK_VERIFIEDshort-circuit ("already checked"). That is exactly the pathflatctakes (src/flatc.cpp:907), soflatc --flexbuffers --jsondies on the input:A direct
ToString()call under ASan shows the mutual recursion exhausting the stack — 248 alternatingReference::ToString/AppendToString<Vector>frames (flexbuffers.h:664↔:386) before the guard page:A deeply nested acyclic buffer (100k levels, built with the regular
Builder) crashes the same way.ToString()is a debug printer and does not require the buffer to have been verified at all, so it has to be safe on its own.Fix
Cap the recursion at
kToStringMaxDepth = 64and truncate with"..."(see diff: the constant at the top ofReference::ToString, the check just before the map branch, guarded onIsAnyVector()which covers map/vector/typed-vector/fixed-typed-vector — the same set#9078enumerated). The cap lives inToString()rather thanAppendToString()so the map branch is covered by the same check;cur_indentalready tracks the depth, so no new parameter is needed. Depth 64 bounds the recursion at ~128 stack frames, safe on any thread stack.Scope
This fixes a stack-exhaustion crash (DoS) in the FlexBuffers printer. I have deliberately not claimed more than that: there is no memory corruption and no read/write primitive here, and this patch does not change what the verifier accepts.
The verifier's reuse tracker still admits cyclic buffers — that is a separate, verifier-side defect being worked in #9187 and #9440 (with the verifier depth-accounting family #9442 / #9421 / #9197 / #9097 alongside). None of those bound
ToStringitself, and conversely this patch does not fix verification: an application that verifies with a reuse tracker will still accept a cyclic buffer, it will just be able to print it safely afterwards. Both layers should land; this PR is the printer layer.Regression test
FlexBuffersToStringRecursionTest()intests/flexbuffers_test.cpp:[ 1, [ 2 ] ]);"..."-truncated output. Both probes run in a forked child process (in-process on Windows), so an uncapped build fails the test by crashing the child with the stack overflow instead of taking down the test runner.Verification
Dual-tree
ctest, same suite in both states, only the new test flips:flattestsFAILED (2 assertions: cyclic probe, deep probe)flattestsPassed,ALL TESTS PASSEDEnd-to-end, before:
flatc --flexbuffers --jsonSIGSEGV (exit 139) on the cyclic input. After: clean exit, 259-byte truncated JSON (64 nested[+"..."+ 64 closers). ASan, before:stack-overflowwith 248 recursion frames. After: clean exit on the cyclic buffer, on the 200k-deep buffer, and on benign buffers, whose rendering is unchanged. Every pre-existing test passes in both trees.Category: Secure-by-design memory safety improvements.
Supersedes #9078 — the author has been inactive since 2026-05-06 with no engagement on the PR or the issue. The fix is re-derived at master (f58d576) with the same semantics (
kToStringMaxDepth = 64,"..."truncation inReference::ToString) and adds a regression test; I'll leave a note on that PR.