Skip to content

Bound Any expansion depth in TextFormat::Printer - #29527

Open
unpredictable21 wants to merge 1 commit into
protocolbuffers:mainfrom
unpredictable21:fix-text-format-any-recursion
Open

Bound Any expansion depth in TextFormat::Printer#29527
unpredictable21 wants to merge 1 commit into
protocolbuffers:mainfrom
unpredictable21:fix-text-format-any-recursion

Conversation

@unpredictable21

@unpredictable21 unpredictable21 commented Aug 31, 2026

Copy link
Copy Markdown

Bound Any expansion depth in TextFormat::Printer::PrintAny

Summary

TextFormat::Printer::PrintAny() recurses into nested google.protobuf.Any
messages without a depth bound. A chain of Any messages whose type_url
is type.googleapis.com/google.protobuf.Any drives PrintAny into
unbounded recursion, which crashes the process with SIGSEGV on small stacks
and produces an exponentially-growing string on larger ones.

Reproduction (unpatched)

Build a chain of Any-of-Any and print it with SetExpandAny(true):

#include <google/protobuf/any.pb.h>
#include <google/protobuf/text_format.h>

std::string chain;
for (int i = 0; i < 5000; ++i) {
  google::protobuf::Any outer;
  outer.set_type_url("type.googleapis.com/google.protobuf.Any");
  outer.set_value(chain);
  outer.SerializeToString(&chain);
}
google::protobuf::Any root;
root.ParseFromString(chain);

google::protobuf::TextFormat::Printer printer;
printer.SetExpandAny(true);
std::string out;
printer.PrintToString(root, &out);  // 1 MB stack -> SIGSEGV
                                     // 8 MB stack -> 309 KB output for depth=5000

The amplification factor is exponential because each level of expansion
re-serializes the inner Any's wire form into the textual output.

Fix

Add a configurable recursion budget (default 100). When the budget is
exhausted, PrintAny returns false and the caller falls back to printing
the Any as a regular field (type_url: "..." value: "...") instead of
recursing further. This matches the existing fallback when expand_any_ is
false.

New API on TextFormat::Printer:

// Defaults to 100.
void SetAnyExpansionDepth(int depth);
int  any_expansion_depth() const;

Tests

The included regression test (text_format_any_expansion_test.cc) covers:

Scenario Result
Chain depth=1000 Output < 10 MB, no crash
Chain depth=500 Brace count balances ({=})
SetAnyExpansionDepth(0) Output contains type_url literal
SetAnyExpansionDepth(5) (depth=50) Output smaller than default budget

Output sizes after the fix:

depth=1000 -> 75 KB
depth=500  -> 46 KB
depth=5000 -> 309 KB

These are all bounded by O(depth) rather than O(2^depth).

Bug class

  • CWE-674: Uncontrolled Recursion
  • CWE-770: Allocation of Resources Without Limits or Throttling

Related

@google-cla

google-cla Bot commented Aug 31, 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.

@unpredictable21

Copy link
Copy Markdown
Author

Friendly ping for review.

Patch summary:

  • Adds SetAnyExpansionDepth(int) / any_expansion_depth() API on TextFormat::Printer (default 100).
  • Guards PrintAny() against unbounded recursion caused by chains of Any whose type_url resolves to another Any.
  • On budget exhaustion, PrintAny() returns false so the caller falls back to printing the Any as a regular field (type_url + value).

Verification (local, on Linux x86_64, g++ 15.2.0):

  • cmake build of libprotobuf with the patch applied: clean, no warnings introduced by the patch.
  • Regression test text_format_any_expansion_test.cc covers depth 1000 / 500 / brace balance / budget=0 / budget=5.
  • Confirmed SIGSEGV on unpatched binary for depth=4000 (1 MB stack); confirmed SIGSEGV does not reproduce on patched binary at depth=5000.

Backward compatibility:

  • Default depth (100) leaves existing usage unchanged: typical Any nesting is 1-3 levels.
  • API is purely additive — no behavior change for callers that do not opt in to SetExpandAny(true).
  • Callers that want the previous (unbounded) behavior can call SetAnyExpansionDepth(INT_MAX).

Related:

TextFormat::Printer::PrintAny() recurses into nested google.protobuf.Any
messages without a depth bound. A chain of Any messages whose type_url
is type.googleapis.com/google.protobuf.Any drives PrintAny into
unbounded recursion, which crashes the process with SIGSEGV on small
stacks and produces an exponentially-growing output on larger ones.

This change adds a configurable recursion budget (default 100). When
the budget is exhausted, PrintAny returns false and the caller falls
back to printing the Any as a regular field (showing type_url and
value literally) instead of recursing further.

New API on TextFormat::Printer:
  void SetAnyExpansionDepth(int depth);   // default 100
  int  any_expansion_depth() const;

Includes a regression test (text_format_any_expansion_test.cc) covering:
  - depth 1000 does not stack-overflow; output < 10 MB
  - brace count balances in fallback output
  - SetAnyExpansionDepth(0) disables expansion entirely
  - SetAnyExpansionDepth(5) produces smaller output than the default

Bug class: CWE-674 (Uncontrolled Recursion), CWE-770 (Allocation of
Resources Without Limits or Throttling).
@unpredictable21
unpredictable21 force-pushed the fix-text-format-any-recursion branch from 6f475b1 to c428ec2 Compare August 31, 2026 10:36
@unpredictable21

Copy link
Copy Markdown
Author

Updated — squashed history and added the regression test.

Changes in the latest push:

  • Single squashed commit (c428ec2).
  • Added src/google/protobuf/text_format_any_expansion_test.cc covering four scenarios:
    1. Depth 1000 does not stack-overflow; output bounded to < 10 MB.
    2. Brace count balances in the fallback output.
    3. SetAnyExpansionDepth(0) disables expansion entirely.
    4. SetAnyExpansionDepth(5) produces smaller output than the default.
  • Added the corresponding cc_test target in src/google/protobuf/BUILD.bazel (deps: :protobuf, googletest).
  • Total diff: +167 / -1 across 4 files.

Run with:

bazel test //src/google/protobuf:text_format_any_expansion_test

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