Skip to content

Make FieldMaskTree walks over deep mask trees stack safe - #30447

Open
ManoharPaturi wants to merge 1 commit into
protocolbuffers:mainfrom
ManoharPaturi:fix-fieldmask-deep-recursion
Open

ManoharPaturi wants to merge 1 commit into
protocolbuffers:mainfrom
ManoharPaturi:fix-fieldmask-deep-recursion

Conversation

@ManoharPaturi

Copy link
Copy Markdown

Nothing bounds the depth of a FieldMask path. AddPath() builds the tree iteratively and accepts arbitrarily deep dotted paths, and ClearChildren(), ForEachLeaf() and the node destructor in field_mask_util.cc were already written iteratively with comments noting they must stay stack safe on very deep trees. Two walks did not follow that pattern and recursed once per tree level: FieldMaskTree::MergeMessage and FieldMaskTree::AddRequiredFieldPath.

Because AddPath() performs no schema validation, a hostile mask over any message with a recursive optional field is enough to drive the recursion. For example, one hundred thousand dotted "a" segments against proto2_unittest::TestRecursiveMessage makes FieldMaskUtil::MergeMessageTo overflow the stack, since MergeMessage descends one frame per tree level and materializes a submessage at each step. AddRequiredFieldPath, reached through FieldMaskUtil::TrimMessage with keep_required_fields, recurses the same way over the tree even when the message itself is shallow.

Both walks now use an explicit work stack, mirroring the style of ClearChildren() and ForEachLeaf() in the same file. Processing order within a node is unchanged; only the order in which independent subtrees are visited differs, and each visited field of a given message is distinct, so the merge and trim results are identical.

Testing: added FieldMaskUtilTest.StackSafeDeepMaskPath, which merges a 100000 segment mask into arena allocated TestRecursiveMessage instances and then trims with keep_required_fields set. Before the change the test process dies with SIGSEGV (exit 139) from stack exhaustion inside MergeMessage; with the change the full FieldMaskUtilTest suite passes, 22 tests locally. The arena matters only for teardown of the very deep destination chain that the merge materializes.

Nothing bounds the depth of a FieldMask path, so a hostile mask such as
one hundred thousand dotted segments over a recursive field drives
unbounded recursion in FieldMaskTree::MergeMessage and
AddRequiredFieldPath and overflows the stack.  Convert both walks to
explicit work stacks, mirroring what ClearChildren() and ForEachLeaf()
in this file already do to stay stack safe on deep trees, and add a
regression test that crashed with SIGSEGV before the change.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant