fastavro: unbounded native recursion decoding a recursive-schema Avro container file causes C-stack overflow (SIGSEGV / DoS)
Target: fastavro β fast Avro implementation for Python
Affected version: fastavro 1.12.2 (latest release at time of report)
Component: compiled Cython reader β fastavro/_read.pyx β _read.cpython-313-x86_64-linux-gnu.so (the default fastavro.reader() API)
Class: CWE-674 Uncontrolled Recursion β C-stack exhaustion β SIGSEGV (Denial of Service)
Impact: An attacker-supplied .avro object-container file (~5 KB) crashes the Python interpreter (SIGSEGV) of any application that decodes it with the standard fastavro.reader(). No reader-side schema cooperation is required β the malicious schema travels inside the file header.
Root cause
An Avro object-container file embeds its own writer schema inside its header metadata under the avro.schema key. fastavro reads that schema out of the file and uses it to decode the data blocks. This means an attacker fully controls the decoding schema.
fastavro decodes record and union values by mutual recursion, with no depth limit. In the mirror pure-Python source (fastavro/_read_py.py, same logic as the compiled _read.pyx):
read_union(line ~406) selects a branch and callsread_data(...)on it.read_record(line ~496) iterates the record's fields and callsread_data(...)for each field's type.
So for a self-recursive record schema the decode call chain is:
read_record -> _read_data -> read_union -> _read_data -> read_record -> ...
one native frame per nesting level. The compiled Cython extension does not participate in Python's sys.setrecursionlimit accounting, so instead of a catchable RecursionError the native C call stack is exhausted, crashing the whole interpreter with SIGSEGV.
Attacker-controlled recursive schema (embedded in the file header)
{"type":"record","name":"N","fields":[{"name":"next","type":["null","N"]}]}
Each nesting level is encoded as a single union-index byte 0x02 (branch index 1 = recurse into N); the chain is terminated by one 0x00 byte (branch index 0 = null). A file with D copies of 0x02 forces D levels of native recursion.
PoC
gen.py hand-builds a valid Avro object-container file with no dependency on fastavro's writer:
- magic
Obj\x01 - metadata map
{avro.schema: <recursive record N>, avro.codec: "null"} - 16-byte sync marker
- one data block:
object_count=1, a bytes-framed payload ofDΓ0x02followed by0x00 - trailing sync marker
$ ./venv/bin/python gen.py crash.avro 50000 # depth D = 50000
$ ./venv/bin/python -c "import fastavro; [None for _ in fastavro.reader(open('crash.avro','rb'))]"
crash.avro is 50,157 bytes. The minimal trigger is ~5 KB (D β 5000).
Captured evidence (verbatim, fastavro 1.12.2, CPython 3.13)
$ ./venv/bin/python -X faulthandler -c "import fastavro; [None for _ in fastavro.reader(open('crash.avro','rb'))]"
Fatal Python error: Segmentation fault
Current thread 0x00007fcb169d1200 (most recent call first):
File "<string>", line 1 in <module>
Extension modules: fastavro._logical_readers, fastavro._schema, fastavro._read, fastavro._logical_writers, fastavro._validation, fastavro._write (total: 6)
Process exits with signal 11 (shell exit code 139).
Threshold / signal (isolated subprocesses)
D=3000 exit=0 (clean decode)
D=5000 exit=139 SIGSEGV
D=8000 exit=139 SIGSEGV
D=12000 exit=139 SIGSEGV
D=20000 exit=139 SIGSEGV
D=50000 exit=139 SIGSEGV
Negative control
shallow.avro (D=5) -> decodes cleanly, exit 0
Pure-Python contrast (isolates the defect to the compiled extension)
Running the same crash.avro through the pure-Python fallback reader raises a catchable RecursionError instead of segfaulting:
$ ./venv/bin/python -c "from fastavro import _read_py; [None for _ in _read_py.reader(open('crash.avro','rb'))]"
RecursionError (catchable)
This confirms the root cause is the compiled C extension (_read), which is what fastavro.reader() uses by default; the pure-Python path is protected by Python's recursion limit, the native path is not.
Verified compiled reader on disk: venv/lib/python3.13/site-packages/fastavro/_read.cpython-313-x86_64-linux-gnu.so.
Impact
Any service that ingests untrusted Avro container files with the standard, documented fastavro.reader() / fastavro.reader(fo) API β data pipelines, message consumers, file upload processors, ML feature stores β can be crashed (interpreter SIGSEGV, hard process kill) by a single ~5 KB file. The attack needs no reader-side schema; the recursive schema is carried inside the file. This is a reliable, low-cost, unauthenticated Denial of Service.
Suggested remediation
Enforce a configurable maximum decode-recursion depth in the Cython reader (mirroring what Python's recursion limit gives the pure-Python path), raising a catchable exception when exceeded β analogous to defenses used by other schema/serialization decoders against self-referential type definitions.
Dedup / prior work
No known CVE for this specific stack-overflow-via-recursive-schema in fastavro at time of report. Distinct from the codec/decompression memory-amplification bug class (a separate finding, "avro-decompression-bomb") and from TFRecord/Avro tooling findings β those concern memory amplification, not native call-stack exhaustion via schema recursion.