From 6906f79b814f1b5b2a02e846b9ea63c702804c1b Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Tue, 25 Aug 2026 00:26:10 -0400 Subject: [PATCH] json: raise wuffs cursor nesting bound to 16 Signed-off-by: Boteng Yao --- .../common/json/wuffs_json/wuffs_json_cursor.h | 14 ++++++++------ .../json/wuffs_json/wuffs_json_cursor_test.cc | 18 +++++++++++++----- .../json_with_ext_buf_parser_test.cc | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/source/common/json/wuffs_json/wuffs_json_cursor.h b/source/common/json/wuffs_json/wuffs_json_cursor.h index e8207008bee88..67cab2e0ff2cd 100644 --- a/source/common/json/wuffs_json/wuffs_json_cursor.h +++ b/source/common/json/wuffs_json/wuffs_json_cursor.h @@ -241,11 +241,13 @@ class WuffsJsonCursor { bool wuffs_done_{false}; // Exclusive upper bound for per-depth state tracking: depths 1 through - // kMaxTrackedDepth-1 (currently 1–8) have full key/dup/path tracking. - // Value covers the deepest known OpenAI/Anthropic schema paths: - // tools[i].function.parameters.properties..type (depth 7) - // messages[i].content[j].content[k].text (depth 7) - // plus one buffer level for schemas with one extra level of nesting. + // kMaxTrackedDepth-1 (currently 1–16) have full key/dup/path tracking. + // Value covers the deepest known provider schema paths: + // tools[i].function.parameters.properties..type (depth 7) + // messages[i].content[j].content[k].text (depth 7) + // [i].candidates[j].content.parts[k].functionCall.args.* (depth 9+, + // Gemini streamed root arrays; `args` is an arbitrary object) + // plus headroom for nested tool arguments. // // Nesting beyond kMaxTrackedDepth-1 is rejected with InvalidArgumentError. // Key/dup/path tracking accuracy is bounded by kMaxTrackedDepth-1 because @@ -256,7 +258,7 @@ class WuffsJsonCursor { // losing tracking accuracy. This removes the hard compile-time cap at the // cost of per-push heap allocation; evaluate against the request-path perf // budget before doing so. - static constexpr int kMaxTrackedDepth = 9; + static constexpr int kMaxTrackedDepth = 17; // Cap key length at 256 bytes: well above any legitimate schema field name // (longest observed ~25B, e.g. "input_audio_transcription") while bounding // per-key allocation and guarding against DoS via unbounded key lengths. diff --git a/test/common/json/wuffs_json/wuffs_json_cursor_test.cc b/test/common/json/wuffs_json/wuffs_json_cursor_test.cc index a9b95c9fc0dbc..a8a28423c0c47 100644 --- a/test/common/json/wuffs_json/wuffs_json_cursor_test.cc +++ b/test/common/json/wuffs_json/wuffs_json_cursor_test.cc @@ -598,17 +598,25 @@ TEST(WuffsJsonCursorTest, ByteRangeKeyValueFieldString) { // Depth limit tests -// 8 levels of nesting must be accepted (boundary value for default max_depth=8). +// 16 levels of nesting must be accepted (boundary value for the tracked +// depth limit; Gemini streamed root arrays reach depth 9+ through +// candidates[].content.parts[].functionCall.args). TEST(WuffsJsonCursorTest, MaxDepthAccepted) { CapturingHandler h; - // 8 nested objects, scalar at the innermost level. - EXPECT_OK(parse(R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}})", h)); + // 16 nested objects, scalar at the innermost level. + EXPECT_OK(parse( + R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}}}}}}}}})", + h)); } -// 9 levels of nesting must be rejected (fail-closed). +// 17 levels of nesting must be rejected (fail-closed). TEST(WuffsJsonCursorTest, ExceedMaxDepthRejected) { CapturingHandler h; - EXPECT_THAT(parse(R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}})", h), Not(IsOk())); + EXPECT_THAT( + parse( + R"({"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}}}}}}}}}})", + h), + Not(IsOk())); } // Path-tracking tests diff --git a/test/extensions/filters/http/ai_protocol_manager/json_with_ext_buf_parser_test.cc b/test/extensions/filters/http/ai_protocol_manager/json_with_ext_buf_parser_test.cc index c13e42134f5f9..031b535cde400 100644 --- a/test/extensions/filters/http/ai_protocol_manager/json_with_ext_buf_parser_test.cc +++ b/test/extensions/filters/http/ai_protocol_manager/json_with_ext_buf_parser_test.cc @@ -372,6 +372,20 @@ TEST_F(JsonWithExtBufParserTest, MixedInlineAndOffloadedValues) { EXPECT_LT(system_ref.offset, user_ref.offset); } +// Gemini streamed root arrays reach candidates[].content.parts[].functionCall +// .args (depth 9+, and `args` is an arbitrary object): well within the raised +// nesting bound. +TEST_F(JsonWithExtBufParserTest, GeminiFunctionCallDepthParses) { + JsonWithExtBufParser parser({}); + const std::string body = + R"([{"candidates":[{"content":{"parts":[{"functionCall":{"name":"f",)" + R"("args":{"location":{"city":"sf"}}}}]}}],)" + R"("usageMetadata":{"promptTokenCount":6,"candidatesTokenCount":9,"totalTokenCount":15}}])"; + ASSERT_TRUE(parser.feed(body, /*end_stream=*/true).ok()); + const auto doc = parser.takeDocument(); + EXPECT_EQ(doc.json()[0]["usageMetadata"]["totalTokenCount"], 15); +} + } // namespace } // namespace AiProtocolManager } // namespace HttpFilters