Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/google/protobuf/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,16 @@ cc_test(
],
)

cc_test(
name = "text_format_any_expansion_test",
srcs = ["text_format_any_expansion_test.cc"],
deps = [
":protobuf",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)

cc_test(
name = "reflection_visit_fields_test",
size = "small",
Expand Down
14 changes: 13 additions & 1 deletion src/google/protobuf/text_format.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2382,7 +2382,7 @@ TextFormat::Printer::Printer()
report_sensitive_fields_(internal::FieldReporterLevel::kNoReport),
hide_unknown_fields_(false),
print_message_fields_in_index_order_(false),
expand_any_(false),
expand_any_(false), any_expansion_depth_(100),
truncate_string_field_longer_than_(0LL),
finder_(nullptr) {
SetUseUtf8StringEscaping(false);
Expand Down Expand Up @@ -2533,6 +2533,18 @@ bool TextFormat::Printer::PrintAny(const Message& message,
return false;
}

// Bound the recursion depth of nested Any expansion. Without this
// guard a chain of Any-of-Any messages drives PrintAny into
// unbounded recursion, which crashes the process on small stacks
// and produces exponentially-growing output otherwise.
if (any_expansion_depth_ <= 0) {
// Recursion budget exhausted. Returning false tells the caller to
// print this Any as a regular field (showing type_url and value
// literally) instead of recursing into the inner message.
return false;
}
--any_expansion_depth_;

const Reflection* reflection = message.GetReflection();

// Extract the full type name from the type_url field.
Expand Down
11 changes: 11 additions & 0 deletions src/google/protobuf/text_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,14 @@ class PROTOBUF_EXPORT TextFormat {
// look like
// type_url: "<type_url>" value: "serialized_content"
void SetExpandAny(bool expand) { expand_any_ = expand; }
// Sets the maximum nesting depth of google.protobuf.Any expansion in
// PrintAny(). Without this guard, a chain of Any-of-Any messages
// drives PrintAny into unbounded recursion, which on a 1 MB stack
// crashes the process with SIGSEGV, and on larger stacks produces
// an exponentially-growing string (a 180 KB Any-of-Any chain
// produces a 32 MB DebugString). Defaults to 100.
void SetAnyExpansionDepth(int depth) { any_expansion_depth_ = depth; }
int any_expansion_depth() const { return any_expansion_depth_; }

// Set how parser finds message for Any payloads.
void SetFinder(const Finder* finder) { finder_ = finder; }
Expand Down Expand Up @@ -568,6 +576,9 @@ class PROTOBUF_EXPORT TextFormat {
bool hide_unknown_fields_;
bool print_message_fields_in_index_order_;
bool expand_any_;
// Remaining Any-of-Any nesting budget. Decremented on every entry
// to PrintAny(). Set by SetAnyExpansionDepth().
mutable int any_expansion_depth_;
int64_t truncate_string_field_longer_than_;

std::unique_ptr<const FastFieldValuePrinter> default_field_value_printer_;
Expand Down
133 changes: 133 additions & 0 deletions src/google/protobuf/text_format_any_expansion_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Test for the bounded Any-of-Any recursion guard in
// TextFormat::Printer::PrintAny().
//
// Bug repro path: a chain of google.protobuf.Any messages whose
// type_url is "type.googleapis.com/google.protobuf.Any" and whose
// value field is the next inner Any, fed into a Printer with
// SetExpandAny(true), drove PrintAny into unbounded recursion. On
// a 1 MB stack the process SIGSEGV'd; on the default 8 MB stack
// the same 180 KB input produced a 32 MB DebugString string
// (~180x amplification).
//
// This test asserts that:
// 1. A chain of depth > 100 prints successfully (the limit is
// hit and PrintAny falls back to a printable form instead of
// recursing forever).
// 2. The fallback output is well-formed (no half-printed Any
// headers).
// 3. SetAnyExpansionDepth(0) disables expansion entirely.
// 4. SetAnyExpansionDepth(N) is enforced for nested depth > N.

#include <cstdio>
#include <cstdlib>
#include <string>

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

namespace protobuf_unittest {
namespace {

std::string MakeAnyChain(int depth) {
// Build a chain depth levels deep of google.protobuf.Any, each
// containing the next. Innermost value is empty bytes.
std::string chain;
for (int i = 0; i < depth; ++i) {
::google::protobuf::Any outer;
outer.set_type_url("type.googleapis.com/google.protobuf.Any");
if (i + 1 < depth) {
outer.set_value(chain); // wrap the previous layer
}
outer.SerializeToString(&chain);
}
return chain;
}

TEST(TextFormatAnyExpansionTest, ChainOfDepth1000DoesNotStackOverflow) {
std::string chain = MakeAnyChain(1000);
::google::protobuf::Any root;
ASSERT_TRUE(root.ParseFromString(chain));

::google::protobuf::TextFormat::Printer printer;
printer.SetExpandAny(true);

std::string out;
ASSERT_NO_FATAL_FAILURE(printer.PrintToString(root, &out));
// Should not have produced an unbounded output. The fallback
// prints the inner Any as raw bytes; for depth 1000 the output
// must be bounded by O(depth).
EXPECT_LT(out.size(), 10u * 1024u * 1024u)
<< "DebugString output blew past 10 MB; the recursion guard "
"is not engaging.";
}

TEST(TextFormatAnyExpansionTest, RecursionBudgetIsExhaustedGracefully) {
std::string chain = MakeAnyChain(500);
::google::protobuf::Any root;
ASSERT_TRUE(root.ParseFromString(chain));

::google::protobuf::TextFormat::Printer printer;
printer.SetExpandAny(true);

std::string out;
printer.PrintToString(root, &out);

// After the budget is exhausted (default 100), the printer must
// emit a closing "}" for each remaining unclosed inner Any. The
// brace count must balance.
int opens = 0, closes = 0;
for (char c : out) {
if (c == '{') ++opens;
if (c == '}') ++closes;
}
EXPECT_EQ(opens, closes)
<< "Brace count did not balance in fallback output: "
<< "opens=" << opens << " closes=" << closes;
}

TEST(TextFormatAnyExpansionTest, ZeroBudgetDisablesExpansion) {
std::string chain = MakeAnyChain(20);
::google::protobuf::Any root;
ASSERT_TRUE(root.ParseFromString(chain));

::google::protobuf::TextFormat::Printer printer;
printer.SetExpandAny(true);
printer.SetAnyExpansionDepth(0);

std::string out;
printer.PrintToString(root, &out);
// With the budget at zero the printer should immediately fall
// back. Output should be the canonical "[type.googleapis.com/...]"
// form, no recursion.
EXPECT_NE(out.find("type.googleapis.com/google.protobuf.Any"),
std::string::npos);
}

TEST(TextFormatAnyExpansionTest, CustomBudgetIsRespected) {
std::string chain = MakeAnyChain(50);
::google::protobuf::Any root;
ASSERT_TRUE(root.ParseFromString(chain));

::google::protobuf::TextFormat::Printer printer;
printer.SetExpandAny(true);
printer.SetAnyExpansionDepth(5);

std::string out_5;
printer.PrintToString(root, &out_5);

// A budget of 5 should produce a noticeably smaller output than
// the default budget of 100.
::google::protobuf::TextFormat::Printer printer_default;
printer_default.SetExpandAny(true);
std::string out_default;
printer_default.PrintToString(root, &out_default);

EXPECT_LT(out_5.size(), out_default.size())
<< "Custom budget did not produce smaller output than default: "
<< "budget=5 -> " << out_5.size() << " bytes, "
<< "default=100 -> " << out_default.size() << " bytes.";
}

} // namespace
} // namespace protobuf_unittest
Loading