This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new f1bf0fc93 feat(python): support limit pyfory depth (#2625)
f1bf0fc93 is described below
commit f1bf0fc93c7f2b44ed78c5424c59e9d892aba308
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Sep 17 23:10:38 2025 +0800
feat(python): support limit pyfory depth (#2625)
## Why?
<!-- Describe the purpose of this PR. -->
## What does this PR do?
support limit pyfory depth
## Related issues
<!--
Is there any related issue? If this PR closes them you say say
fix/closes:
- #xxxx0
- #xxxx1
- Fixes #xxxx2
-->
## Does this PR introduce any user-facing change?
<!--
If any user-facing interface changes, please [open an
issue](https://github.com/apache/fory/issues/new/choose) describing the
need to do so and update the document if necessary.
Delete section if not applicable.
-->
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
<!--
When the PR has an impact on performance (if you don't know whether the
PR will have an impact on performance, you can submit the PR first, and
if it will have impact on performance, the code reviewer will explain
it), be sure to attach a benchmark data here.
Delete section if not applicable.
-->
---
python/pyfory/_fory.py | 34 ++++++++++++++++++++++++++++++++--
python/pyfory/_serialization.pyx | 32 ++++++++++++++++++++++++++++++--
2 files changed, 62 insertions(+), 4 deletions(-)
diff --git a/python/pyfory/_fory.py b/python/pyfory/_fory.py
index 0d7f6adbe..586f92760 100644
--- a/python/pyfory/_fory.py
+++ b/python/pyfory/_fory.py
@@ -113,6 +113,8 @@ class Fory:
"_unsupported_callback",
"_unsupported_objects",
"_peer_language",
+ "max_depth",
+ "depth",
)
def __init__(
@@ -121,6 +123,7 @@ class Fory:
ref_tracking: bool = False,
require_type_registration: bool = True,
compatible: bool = False,
+ max_depth: int = 50,
):
"""
:param require_type_registration:
@@ -134,6 +137,10 @@ class Fory:
:param compatible:
Whether to enable compatible mode for cross-language serialization.
When enabled, type forward/backward compatibility for struct fields
will be enabled.
+ :param max_depth:
+ The maximum depth of the deserialization data.
+ If the depth exceeds the maximum depth, an exception will be raised.
+ The default value is 50.
"""
self.language = language
self.is_py = language == Language.PYTHON
@@ -169,6 +176,8 @@ class Fory:
self._unsupported_callback = None
self._unsupported_objects = None
self._peer_language = None
+ self.max_depth = max_depth
+ self.depth = 0
def register(
self,
@@ -417,7 +426,11 @@ class Fory:
# indicates that the object is first read.
if ref_id >= NOT_NULL_VALUE_FLAG:
typeinfo = self.type_resolver.read_typeinfo(buffer)
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
o = typeinfo.serializer.read(buffer)
+ self.depth -= 1
ref_resolver.set_read_object(ref_id, o)
return o
else:
@@ -426,7 +439,12 @@ class Fory:
def deserialize_nonref(self, buffer):
"""Deserialize not-null and non-reference object from buffer."""
typeinfo = self.type_resolver.read_typeinfo(buffer)
- return typeinfo.serializer.read(buffer)
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
+ o = typeinfo.serializer.read(buffer)
+ self.depth -= 1
+ return o
def xdeserialize_ref(self, buffer, serializer=None):
if serializer is None or serializer.need_to_write_ref:
@@ -447,7 +465,12 @@ class Fory:
def xdeserialize_nonref(self, buffer, serializer=None):
if serializer is None:
serializer = self.type_resolver.read_typeinfo(buffer).serializer
- return serializer.xread(buffer)
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
+ o = serializer.xread(buffer)
+ self.depth -= 1
+ return o
def write_buffer_object(self, buffer, buffer_object: BufferObject):
if self._buffer_callback is None or
self._buffer_callback(buffer_object):
@@ -513,6 +536,7 @@ class Fory:
self._unsupported_callback = None
def reset_read(self):
+ self.depth = 0
self.ref_resolver.reset_read()
self.type_resolver.reset_read()
self.serialization_context.reset_read()
@@ -525,6 +549,12 @@ class Fory:
self.reset_write()
self.reset_read()
+ def throw_depth_limit_exceeded_exception(self):
+ raise Exception(
+ f"Read depth exceed max depth: {self.depth}, the deserialization
data may be malicious. If it's not malicious, "
+ "please increase max read depth by Fory(..., max_depth=...)"
+ )
+
_ENABLE_TYPE_REGISTRATION_FORCIBLY =
os.getenv("ENABLE_TYPE_REGISTRATION_FORCIBLY", "0") in {
"1",
diff --git a/python/pyfory/_serialization.pyx b/python/pyfory/_serialization.pyx
index 1320a9f4a..6754cc4af 100644
--- a/python/pyfory/_serialization.pyx
+++ b/python/pyfory/_serialization.pyx
@@ -799,6 +799,8 @@ cdef class Fory:
cdef object _unsupported_callback
cdef object _unsupported_objects # iterator
cdef object _peer_language
+ cdef int32_t max_depth
+ cdef int32_t depth
def __init__(
self,
@@ -806,6 +808,7 @@ cdef class Fory:
ref_tracking: bool = False,
require_type_registration: bool = True,
compatible: bool = False,
+ max_depth: int = 50,
):
"""
:param require_type_registration:
@@ -819,6 +822,10 @@ cdef class Fory:
:param compatible:
Whether to enable compatible mode for cross-language serialization.
When enabled, type forward/backward compatibility for struct fields
will be enabled.
+ :param max_depth:
+ The maximum depth of the deserialization data.
+ If the depth exceeds the maximum depth, an exception will be raised.
+ The default value is 50.
"""
self.language = language
if _ENABLE_TYPE_REGISTRATION_FORCIBLY or require_type_registration:
@@ -851,6 +858,8 @@ cdef class Fory:
self._unsupported_callback = None
self._unsupported_objects = None
self._peer_language = None
+ self.depth = 0
+ self.max_depth = max_depth
def register_serializer(self, cls: Union[type, TypeVar], Serializer
serializer):
self.type_resolver.register_serializer(cls, serializer)
@@ -1116,7 +1125,11 @@ cdef class Fory:
return buffer.read_bool()
elif cls is float:
return buffer.read_double()
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
o = typeinfo.serializer.read(buffer)
+ self.depth -= 1
ref_resolver.set_read_object(ref_id, o)
return o
@@ -1132,7 +1145,12 @@ cdef class Fory:
return buffer.read_bool()
elif cls is float:
return buffer.read_double()
- return typeinfo.serializer.read(buffer)
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
+ o = typeinfo.serializer.read(buffer)
+ self.depth -= 1
+ return o
cpdef inline xdeserialize_ref(self, Buffer buffer, Serializer
serializer=None):
cdef MapRefResolver ref_resolver
@@ -1160,7 +1178,16 @@ cdef class Fory:
self, Buffer buffer, Serializer serializer=None):
if serializer is None:
serializer = self.type_resolver.read_typeinfo(buffer).serializer
- return serializer.xread(buffer)
+ self.depth += 1
+ if self.depth > self.max_depth:
+ self.throw_depth_limit_exceeded_exception()
+ o = serializer.xread(buffer)
+ self.depth -= 1
+ return o
+
+ cdef inline throw_depth_limit_exceeded_exception(self):
+ raise Exception(f"Read depth exceed max depth: {self.depth}, the
deserialization data may be malicious. If it's not malicious, "
+ "please increase max read depth by Fory(..., max_depth=...)")
cpdef inline write_buffer_object(self, Buffer buffer, buffer_object):
if self._buffer_callback is not None and
self._buffer_callback(buffer_object):
@@ -1232,6 +1259,7 @@ cdef class Fory:
self._unsupported_callback = None
cpdef inline reset_read(self):
+ self.depth = 0
self.ref_resolver.reset_read()
self.type_resolver.reset_read()
self.metastring_resolver.reset_read()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]