Skip to content

AVRO-4328: [python] Bound decode recursion depth - #3929

Open
iemejia wants to merge 2 commits into
apache:mainfrom
iemejia:AVRO-4328-python-bound-decode-recursion-depth
Open

AVRO-4328: [python] Bound decode recursion depth#3929
iemejia wants to merge 2 commits into
apache:mainfrom
iemejia:AVRO-4328-python-bound-decode-recursion-depth

Conversation

@iemejia

@iemejia iemejia commented Aug 7, 2026

Copy link
Copy Markdown
Member

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 input
that nests deeper than the limit with a bounded AvroException instead of a
RecursionError / crash. 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, reset at
the start of each top-level read(), and restored on exit so a reader can be
reused.

Verifying this change

This change added tests and can be verified as follows:

  • Added test_decode_recursion_depth.py: a ~100k-deep recursive linked-list
    payload is rejected with a bounded AvroException (not a RecursionError),
    a moderately nested value within the limit still decodes, and the
    AVRO_MAX_DECODE_DEPTH override is honored.
  • ruff check, ruff format and mypy --strict pass.

Documentation

  • Does this pull request introduce a new feature? (no — DoS hardening)
  • If yes, how is the feature documented? (docstrings — the new
    AVRO_MAX_DECODE_DEPTH limit is documented alongside the existing collection
    limits in avro/io.py)

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) in DatumReader.read_data.
  • Introduce _nested_read() depth tracking with reset per top-level read(), producing a bounded AvroException on excess nesting.
  • Add test_decode_recursion_depth.py to 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.

Comment thread lang/py/avro/test/test_decode_recursion_depth.py Outdated
Comment thread lang/py/avro/io.py Outdated
Comment thread lang/py/avro/io.py
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants