AVRO-4328: [python] Bound decode recursion depth - #3929
Open
iemejia wants to merge 2 commits into
Open
Conversation
Recursive schemas (e.g. a linked list or tree) let a small, hostile payload drive arbitrarily deep nesting during binary decoding, exhausting the Python call stack (RecursionError, or a fatal interpreter crash once the C stack is exhausted) before any allocation limit is reached. Bound the decode nesting depth by counting structural descents into records, arrays, maps and unions and rejecting input that nests deeper than the limit with a clean AvroException. The default is 100 (matching Protocol Buffers and the Java SDK) and is configurable via the AVRO_MAX_DECODE_DEPTH environment variable. The depth is tracked per DatumReader for the current datum and reset at the start of each top-level read(), and restored on exit so a reader can be reused.
There was a problem hiding this comment.
Pull request overview
This PR hardens the Python Avro binary decoder against recursion-based DoS by bounding structural decode nesting depth in DatumReader.read_data, turning otherwise stack-exhausting inputs into a bounded AvroException. It also adds a focused regression test for deeply recursive payloads and for the AVRO_MAX_DECODE_DEPTH configuration override.
Changes:
- Add a configurable maximum decode nesting depth (
AVRO_MAX_DECODE_DEPTH, default 100) and enforce it during structural descents (union/array/map/record) inDatumReader.read_data. - Introduce
_nested_read()depth tracking with reset per top-levelread(), producing a boundedAvroExceptionon excess nesting. - Add
test_decode_recursion_depth.pyto validate rejection of recursion bombs, successful decode within limits, and env var override behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| lang/py/avro/io.py | Adds decode depth configuration and enforces depth limits during structural decoding paths. |
| lang/py/avro/test/test_decode_recursion_depth.py | Adds regression tests for recursion depth enforcement and env override behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address review feedback: - Guard the structural skip path (skip_array/skip_map/skip_union/ skip_record) with the same _nested_read depth bound, so skipping a writer-only field for a recursive schema during resolution cannot overflow the stack. Add a regression test. - Compute _max_decode_depth() once in _nested_read so the check and the error message cannot disagree if the environment changes between calls. - Preserve and restore any pre-existing AVRO_MAX_DECODE_DEPTH value in the env-override test instead of unconditionally deleting it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lang/py/avro/io.py:929
- The decode-depth budget is incremented for writer+reader unions (via read_union), but not for the schema-resolution path where only the reader schema is a union (writer is not). That resolution path recurses via
return self.read_data(writers_schema, s, decoder)and adds another Python stack frame per union encounter, so deeply nested data can exceed the intended depth budget (and can reach RecursionError earlier than expected) because these union descents are currently uncounted.
if isinstance(writers_schema, avro.schema.UnionSchema) and isinstance(readers_schema, avro.schema.UnionSchema):
with self._nested_read():
return self.read_union(writers_schema, readers_schema, decoder)
if isinstance(readers_schema, avro.schema.UnionSchema):
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the purpose of the change
Python SDK implementation of AVRO-4302 (parent). Recursive schemas (e.g. a
linked list or tree) let a small, hostile payload drive arbitrarily deep
nesting during binary decoding, exhausting the Python call stack
(
RecursionError, or a fatal interpreter crash once the C stack is exhausted)before any allocation limit is reached.
This bounds the decode nesting depth by counting structural descents into
records, arrays, maps and unions in
DatumReader.read_data, rejecting inputthat nests deeper than the limit with a bounded
AvroExceptioninstead of aRecursionError/ crash. The default is 100 (matching Protocol Buffers and theJava SDK) and is configurable via the
AVRO_MAX_DECODE_DEPTHenvironmentvariable. The depth is tracked per
DatumReaderfor the current datum, reset atthe start of each top-level
read(), and restored on exit so a reader can bereused.
Verifying this change
This change added tests and can be verified as follows:
test_decode_recursion_depth.py: a ~100k-deep recursive linked-listpayload is rejected with a bounded
AvroException(not aRecursionError),a moderately nested value within the limit still decodes, and the
AVRO_MAX_DECODE_DEPTHoverride is honored.ruff check,ruff formatandmypy --strictpass.Documentation
AVRO_MAX_DECODE_DEPTHlimit is documented alongside the existing collectionlimits in
avro/io.py)