From 9bf0795f5137a98f05e64bc02c93f342a10df96b Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Thu, 7 May 2026 14:19:26 -0400 Subject: [PATCH] Standardize depth enforcement. Use `defer` to avoid accidentally missing a call on a `return` path. This also counts "Value" as an object; that effectively halves the depth of some raw JSON parsed into a "Struct", but it was allowing what might be a stack bomb attempt to be parsed in some cases. --- .../Google_Protobuf_ListValue+Extensions.swift | 8 ++++---- .../SwiftProtobuf/Google_Protobuf_Value+Extensions.swift | 6 ++++++ Tests/SwiftProtobufTests/Test_Struct.swift | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftProtobuf/Google_Protobuf_ListValue+Extensions.swift b/Sources/SwiftProtobuf/Google_Protobuf_ListValue+Extensions.swift index 559b99dd8..8b8a66c09 100644 --- a/Sources/SwiftProtobuf/Google_Protobuf_ListValue+Extensions.swift +++ b/Sources/SwiftProtobuf/Google_Protobuf_ListValue+Extensions.swift @@ -44,11 +44,12 @@ extension Google_Protobuf_ListValue: _CustomJSONCodable { return } try decoder.scanner.skipRequiredArrayStart() - // Since we override the JSON decoding, we can't rely - // on the default recursion depth tracking. + // When a JSON array becomes a google.protobuf.List, that means we're + // adding an "object" to the depth of objects modeled, so manually + // include that in the recursion usage. try decoder.scanner.incrementRecursionDepth() + defer { decoder.scanner.decrementRecursionDepth() } if decoder.scanner.skipOptionalArrayEnd() { - decoder.scanner.decrementRecursionDepth() return } while true { @@ -56,7 +57,6 @@ extension Google_Protobuf_ListValue: _CustomJSONCodable { try v.decodeJSON(from: &decoder) values.append(v) if decoder.scanner.skipOptionalArrayEnd() { - decoder.scanner.decrementRecursionDepth() return } try decoder.scanner.skipRequiredComma() diff --git a/Sources/SwiftProtobuf/Google_Protobuf_Value+Extensions.swift b/Sources/SwiftProtobuf/Google_Protobuf_Value+Extensions.swift index 65b5b74f7..514b1e353 100644 --- a/Sources/SwiftProtobuf/Google_Protobuf_Value+Extensions.swift +++ b/Sources/SwiftProtobuf/Google_Protobuf_Value+Extensions.swift @@ -76,6 +76,12 @@ extension Google_Protobuf_Value: _CustomJSONCodable { } internal mutating func decodeJSON(from decoder: inout JSONDecoder) throws { + // No matter what was in the JSON, when it becomes a + // google.protobuf.Value, that means we've added an "object" to the + // depth of objects modeled, so manually include that in the recursion + // usage. + try decoder.scanner.incrementRecursionDepth() + defer { decoder.scanner.decrementRecursionDepth() } let c = try decoder.scanner.peekOneCharacter() switch c { case "n": diff --git a/Tests/SwiftProtobufTests/Test_Struct.swift b/Tests/SwiftProtobufTests/Test_Struct.swift index ccc7eddfe..c2708c697 100644 --- a/Tests/SwiftProtobufTests/Test_Struct.swift +++ b/Tests/SwiftProtobufTests/Test_Struct.swift @@ -146,7 +146,8 @@ final class Test_JSON_ListValue: XCTestCase, PBTestHelpers { } func test_JSON_nested_list() throws { - let limit = JSONDecodingOptions().messageDepthLimit + // Every element in a List is a Value, so the max depth is 1/2 of the limit. + let limit = JSONDecodingOptions().messageDepthLimit / 2 let depths = [ // Small lists 1, 2, 3, 4, 5,