Benign MessagePack / RLlib Checkpoint Security PoC
This repository stages a safe proof-of-concept for a MessagePack-based ML checkpoint loading issue. The artifact is a tiny state.msgpack file that follows the Ray RLlib checkpoint state-file shape and carries a NumPy object-dtype array encoded through msgpack-numpy.
When decoded through Ray RLlib's restore_from_path() MessagePack path, the current msgpack-numpy decoder reaches pickle.loads() for object-dtype array data. The embedded payload only writes a local marker file named MSG_PACK_NUMPY_MARKER.txt.
Public PoC URL: https://huggingface.co/pragnyanramtha/rllib-msgpack-numpy-object-array-ace-poc
Files
state.msgpack- benign PoC checkpoint state file.verify_poc.py- verifies plain MessagePack parsing, directmsgpack-numpyparsing, and Ray RLlib restore behavior.build_poc.py- reproduces the artifact generation.artifact_manifest.json- SHA256, size, and marker details.results.json- local verification output.scanner_output_file.json- ModelScan 0.8.8 output forstate.msgpack.scanner_output_dir.json- ModelScan 0.8.8 output for this staged folder.requirements.txt- pinned reproduction dependencies used for this validation.
Tested Versions
- Python 3.12.12
- Ray 2.55.1
- msgpack 1.1.2
- msgpack-numpy 0.4.8
- NumPy 2.4.4
- ModelScan 0.8.8
Reproduction
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt
.venv/Scripts/python build_poc.py
.venv/Scripts/python verify_poc.py
.venv/Scripts/modelscan -p state.msgpack -r json -o scanner_output_file.json --show-skipped
On Linux/macOS, replace .venv/Scripts/python with .venv/bin/python.
Expected behavior:
- Plain
msgpack.load()parses the file as data and does not create the marker. msgpack_numpy.load()createsMSG_PACK_NUMPY_MARKER.txt.- Ray RLlib
Checkpointable.restore_from_path()createsMSG_PACK_NUMPY_MARKER.txt. - ModelScan 0.8.8 reports
total_scanned: 0and skipsstate.msgpackasSCAN_NOT_SUPPORTED.
Evidence Summary
Artifact:
SHA256: 3ddf739096ea87558f341e1705b607510e7e7f3af4c37841b51bd8809b52e465
Size: 506 bytes
Runtime:
"ray_rllib_restore_check": {
"restored_keys": ["format", "object_array", "safe_weights"],
"object_array_type": "ndarray",
"object_array_repr": "array([34], dtype=object)",
"marker_created": true,
"marker_text": "msgpack_numpy_object_array_marker\n"
}
Scanner:
"scanned": {"total_scanned": 0},
"skipped": {
"total_skipped": 1,
"skipped_files": [{
"category": "SCAN_NOT_SUPPORTED",
"description": "Model Scan did not scan file",
"source": "state.msgpack"
}]
}
Why This Is ML-Format Relevant
Ray RLlib documents checkpoints as model/training artifacts that can be saved to local disk or cloud storage and restored through restore_from_path() / from_checkpoint(). The docs state that checkpoint directories contain a pickle or msgpack state file, and current RLlib source loads state.msgpack with a msgpack module patched by msgpack-numpy.
Primary references:
- Ray RLlib checkpoint docs: https://docs.ray.io/en/latest/rllib/checkpoints.html
- Ray RLlib source for
state.msgpackrestore andtry_import_msgpack: https://docs.ray.io/en/latest/_modules/ray/rllib/utils/checkpoints.html - msgpack-numpy 0.4.8 decoder source: https://github.com/lebedov/msgpack-numpy/blob/0.4.8/msgpack_numpy.py
- ModelScan 0.8.8 supported scanner extensions: https://github.com/protectai/modelscan/blob/v0.8.8/modelscan/settings.py
Security Impact
An attacker-controlled RLlib .msgpack checkpoint state file can trigger arbitrary Python execution when a victim restores the checkpoint through RLlib's MessagePack path. This PoC uses a harmless local marker write, but the primitive is Python pickle execution hidden inside a MessagePack/NumPy serialization layer.
Limitations:
- This is not a native parser memory-corruption issue.
- It requires a victim workflow that restores an untrusted Ray RLlib MessagePack checkpoint or otherwise decodes the artifact through
msgpack-numpy. - The scanner evidence is a ModelScan unsupported-format gap for a dangerous
.msgpackartifact, not a claim that every Hugging Face scanner accepts the file as clean.
Mitigations
- Do not restore untrusted RLlib MessagePack checkpoints.
- Reject or sanitize object-dtype arrays during MessagePack checkpoint restore.
- Avoid
msgpack_numpy.patch()for untrusted checkpoint data, or make the object-dtype pickle path opt-in only. - Add scanner support for
.msgpackmodel artifacts that recursively detects nested pickle payloads inmsgpack-numpyobject-array records.