fix: prevent RecursionError when a dict contains itself (#592)
BaseDict._get_dict_or_value() recursively unwraps nested benedict values with no cycle detection, so assigning a benedict into its own nested descendant a second time recurses forever and crashes with RecursionError. Track ids of mappings currently being unwrapped and raise a clear ValueError as soon as a self-reference is detected, instead of recursing into unrelated, equally unprotected traversal code until the interpreter stack overflows.
This commit is contained in:
1 parent
6701da5cf1
commit
88a0720c67
2 files changed
+83
-5
No files matched your search
@@ -22,15 +22,41 @@ class BaseDict(dict[_K, _V]):
|
||||
_dict: dict[_K, _V] | None
|
||||
_frozen: bool
|
||||
|
||||
# ids of mappings currently being unwrapped by `_get_dict_or_value`,
|
||||
# anywhere on the current call stack (not just in directly nested calls
|
||||
# of this method: unwrapping a value can re-enter this method through
|
||||
# __init__/__setitem__/_cast). Used to detect self-referential
|
||||
# (cyclic) structures and abort unwrapping them instead of recursing
|
||||
# forever. fix #592
|
||||
_unwrapping_ids: set[int] = set()
|
||||
|
||||
@classmethod
|
||||
def _get_dict_or_value(cls, value: Any) -> Any:
|
||||
value = value.dict() if isinstance(value, cls) else value
|
||||
if isinstance(value, MutableMapping):
|
||||
for key in value.keys():
|
||||
key_val = value[key]
|
||||
if isinstance(key_val, cls):
|
||||
key_val = cls._get_dict_or_value(value[key])
|
||||
value[key] = key_val
|
||||
value_id = id(value)
|
||||
if value_id in cls._unwrapping_ids:
|
||||
# value is already being unwrapped higher up the call stack,
|
||||
# ie. it (directly or indirectly) contains itself. Raise
|
||||
# immediately instead of unwrapping it again, to unwind the
|
||||
# whole unwrap/cast chain before the still-cyclic structure
|
||||
# can reach unrelated code with no cycle protection of its
|
||||
# own (eg. keypath separator validation), which would also
|
||||
# recurse infinitely but with an uncontrolled RecursionError.
|
||||
raise ValueError(
|
||||
"Cannot assign a dict that contains itself "
|
||||
"(directly or indirectly): self-referential "
|
||||
"(cyclic) structures are not supported."
|
||||
)
|
||||
cls._unwrapping_ids.add(value_id)
|
||||
try:
|
||||
for key in value.keys():
|
||||
key_val = value[key]
|
||||
if isinstance(key_val, cls):
|
||||
key_val = cls._get_dict_or_value(value[key])
|
||||
value[key] = key_val
|
||||
finally:
|
||||
cls._unwrapping_ids.discard(value_id)
|
||||
return value
|
||||
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
||||
|
||||
Reference in new issue
Block a user