Skip to content
Merged
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
250 changes: 250 additions & 0 deletions SPECS/pytorch/CVE-2026-0994.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
From c4eda3e58680528147a4cc7e2b3c9044f795c9c9 Mon Sep 17 00:00:00 2001
From: zhangskz <sandyzhang@google.com>
Date: Thu, 29 Jan 2026 14:31:08 -0500
Subject: [PATCH] Fix Any recursion depth bypass in Python
json_format.ParseDict (#25239) (#25586)

This fixes a security vulnerability where nested google.protobuf.Any messages could bypass the max_recursion_depth limit, potentially leading to denial of service via stack overflow.

The root cause was that _ConvertAnyMessage() was calling itself recursively via methodcaller() for nested well-known types, bypassing the recursion depth tracking in ConvertMessage().

The fix routes well-known type parsing through ConvertMessage() to ensure proper recursion depth accounting for all message types including nested Any.

Fixes #25070
Closes #25239

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/25239 from aviralgarg05:fix-any-recursion-depth-bypass 3cbbcbea142593d3afd2ceba2db14b05660f62f4
PiperOrigin-RevId: 862740421

Co-authored-by: Aviral Garg <gargaviral99@gmail.com>

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: AI Backport of https://github.com/protocolbuffers/protobuf/commit/c4eda3e58680528147a4cc7e2b3c9044f795c9c9.patch
---
.../protobuf/internal/json_format_test.py | 100 ++++++++++++++++++
.../python/google/protobuf/json_format.py | 38 +++++--
2 files changed, 131 insertions(+), 7 deletions(-)

diff --git a/third_party/protobuf/python/google/protobuf/internal/json_format_test.py b/third_party/protobuf/python/google/protobuf/internal/json_format_test.py
index 68aa21c4..69246a91 100755
--- a/third_party/protobuf/python/google/protobuf/internal/json_format_test.py
+++ b/third_party/protobuf/python/google/protobuf/internal/json_format_test.py
@@ -1244,6 +1244,106 @@ class JsonFormatTest(JsonFormatBase):
'uint32Value': 4, 'stringValue': 'bla'},
indent=2, sort_keys=True))

+ def testAnyRecursionDepthEnforcement(self):
+ """Test that nested Any messages respect max_recursion_depth limit."""
+ # Test that deeply nested Any messages raise ParseError instead of
+ # bypassing the recursion limit. This prevents DoS via nested Any.
+ message = any_pb2.Any()
+
+ # Create nested Any structure that should exceed depth limit
+ # With max_recursion_depth=5, we can nest 4 Any messages
+ # (depth 1 = outer Any, depth 2-4 = nested Anys, depth 5 = final value)
+ nested_any = {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {},
+ },
+ },
+ },
+ },
+ }
+
+ # Should raise ParseError due to exceeding max depth, not RecursionError
+ self.assertRaisesRegexp(
+ json_format.ParseError,
+ 'Message too deep. Max recursion depth is 5',
+ json_format.ParseDict,
+ nested_any,
+ message,
+ max_recursion_depth=5,
+ )
+
+ # Verify that Any messages within the limit can be parsed successfully
+ # With max_recursion_depth=5, we can nest up to 4 Any messages
+ shallow_any = {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {},
+ },
+ },
+ },
+ }
+ json_format.ParseDict(shallow_any, message, max_recursion_depth=5)
+
+ def testAnyRecursionDepthBoundary(self):
+ """Test recursion depth boundary behavior (exclusive upper limit)."""
+ message = any_pb2.Any()
+
+ # Create nested Any at depth exactly 4 (should succeed with max_recursion_depth=5)
+ depth_4_any = {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {},
+ },
+ },
+ },
+ }
+ # This should succeed: depth 4 < max_recursion_depth 5
+ json_format.ParseDict(depth_4_any, message, max_recursion_depth=5)
+
+ # Create nested Any at depth exactly 5 (should fail with max_recursion_depth=5)
+ depth_5_any = {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {
+ '@type': 'type.googleapis.com/google.protobuf.Any',
+ 'value': {},
+ },
+ },
+ },
+ },
+ }
+ # This should fail: depth 5 == max_recursion_depth 5 (exclusive limit)
+ self.assertRaisesRegexp(
+ json_format.ParseError,
+ 'Message too deep. Max recursion depth is 5',
+ json_format.ParseDict,
+ depth_5_any,
+ message,
+ max_recursion_depth=5,
+ )

if __name__ == '__main__':
unittest.main()
diff --git a/third_party/protobuf/python/google/protobuf/json_format.py b/third_party/protobuf/python/google/protobuf/json_format.py
index 4d76d021..4147e9e1 100644
--- a/third_party/protobuf/python/google/protobuf/json_format.py
+++ b/third_party/protobuf/python/google/protobuf/json_format.py
@@ -408,7 +408,7 @@ def _CreateMessageFromTypeUrl(type_url, descriptor_pool):
return message_class()


-def Parse(text, message, ignore_unknown_fields=False, descriptor_pool=None):
+def Parse(text, message, ignore_unknown_fields=False, descriptor_pool=None, max_recursion_depth=100):
"""Parses a JSON representation of a protocol message into a message.

Args:
@@ -417,6 +417,9 @@ def Parse(text, message, ignore_unknown_fields=False, descriptor_pool=None):
ignore_unknown_fields: If True, do not raise errors for unknown fields.
descriptor_pool: A Descriptor Pool for resolving types. If None use the
default.
+ max_recursion_depth: max recursion depth of JSON message to be deserialized.
+ JSON messages over this depth will fail to be deserialized. Default value
+ is 100.

Returns:
The same message passed as argument.
@@ -429,13 +432,14 @@ def Parse(text, message, ignore_unknown_fields=False, descriptor_pool=None):
js = json.loads(text, object_pairs_hook=_DuplicateChecker)
except ValueError as e:
raise ParseError('Failed to load JSON: {0}.'.format(str(e)))
- return ParseDict(js, message, ignore_unknown_fields, descriptor_pool)
+ return ParseDict(js, message, ignore_unknown_fields, descriptor_pool, max_recursion_depth)


def ParseDict(js_dict,
message,
ignore_unknown_fields=False,
- descriptor_pool=None):
+ descriptor_pool=None,
+ max_recursion_depth=100):
"""Parses a JSON dictionary representation into a message.

Args:
@@ -444,11 +448,14 @@ def ParseDict(js_dict,
ignore_unknown_fields: If True, do not raise errors for unknown fields.
descriptor_pool: A Descriptor Pool for resolving types. If None use the
default.
+ max_recursion_depth: max recursion depth of JSON message to be deserialized.
+ JSON messages over this depth will fail to be deserialized. Default value
+ is 100.

Returns:
The same message passed as argument.
"""
- parser = _Parser(ignore_unknown_fields, descriptor_pool)
+ parser = _Parser(ignore_unknown_fields, descriptor_pool, max_recursion_depth)
parser.ConvertMessage(js_dict, message)
return message

@@ -459,9 +466,11 @@ _INT_OR_FLOAT = six.integer_types + (float,)
class _Parser(object):
"""JSON format parser for protocol message."""

- def __init__(self, ignore_unknown_fields, descriptor_pool):
+ def __init__(self, ignore_unknown_fields, descriptor_pool, max_recursion_depth):
self.ignore_unknown_fields = ignore_unknown_fields
self.descriptor_pool = descriptor_pool
+ self.max_recursion_depth = max_recursion_depth
+ self.recursion_depth = 0

def ConvertMessage(self, value, message):
"""Convert a JSON object into a message.
@@ -473,6 +482,17 @@ class _Parser(object):
Raises:
ParseError: In case of convert problems.
"""
+ # Increment recursion depth at message entry. The max_recursion_depth limit
+ # is exclusive: a depth value equal to max_recursion_depth will trigger an
+ # error. For example, with max_recursion_depth=5, nesting up to depth 4 is
+ # allowed, but attempting depth 5 raises ParseError.
+ self.recursion_depth += 1
+ if self.recursion_depth > self.max_recursion_depth:
+ raise ParseError(
+ 'Message too deep. Max recursion depth is {0}'.format(
+ self.max_recursion_depth
+ )
+ )
message_descriptor = message.DESCRIPTOR
full_name = message_descriptor.full_name
if _IsWrapperMessage(message_descriptor):
@@ -481,6 +501,7 @@ class _Parser(object):
methodcaller(_WKTJSONMETHODS[full_name][1], value, message)(self)
else:
self._ConvertFieldValuePair(value, message)
+ self.recursion_depth -= 1

def _ConvertFieldValuePair(self, js, message):
"""Convert field value pairs into regular message.
@@ -612,8 +633,11 @@ class _Parser(object):
if _IsWrapperMessage(message_descriptor):
self._ConvertWrapperMessage(value['value'], sub_message)
elif full_name in _WKTJSONMETHODS:
- methodcaller(
- _WKTJSONMETHODS[full_name][1], value['value'], sub_message)(self)
+ # For well-known types (including nested Any), use ConvertMessage
+ # to ensure recursion depth is properly tracked
+ self.ConvertMessage(
+ value['value'], sub_message)
+ )
else:
del value['@type']
self._ConvertFieldValuePair(value, sub_message)
--
2.45.4

6 changes: 5 additions & 1 deletion SPECS/pytorch/pytorch.spec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Summary: Tensors and Dynamic neural networks in Python with strong GPU acceleration.
Name: pytorch
Version: 2.0.0
Release: 13%{?dist}
Release: 14%{?dist}
License: BSD-3-Clause
Vendor: Microsoft Corporation
Distribution: Mariner
Expand All @@ -24,6 +24,7 @@ Patch9: CVE-2025-55552.patch
Patch10: CVE-2025-55560.patch
Patch11: CVE-2025-3001.patch
Patch12: CVE-2026-24747.patch
Patch13: CVE-2026-0994.patch

BuildRequires: cmake
BuildRequires: gcc
Expand Down Expand Up @@ -96,6 +97,9 @@ cp -arf docs %{buildroot}/%{_pkgdocdir}
%{_docdir}/*

%changelog
* Fri Feb 13 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 2.0.0-14
- Patch for CVE-2026-0994

* Thu Jan 29 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 2.0.0-13
- Patch for CVE-2026-24747

Expand Down
Loading