| --- |
| tags: |
| - security |
| - msgpack |
| - rllib |
| - model-format |
| license: other |
| --- |
| |
| # 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, direct `msgpack-numpy` parsing, 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 for `state.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 |
|
|
| ```bash |
| 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()` creates `MSG_PACK_NUMPY_MARKER.txt`. |
| - Ray RLlib `Checkpointable.restore_from_path()` creates `MSG_PACK_NUMPY_MARKER.txt`. |
| - ModelScan 0.8.8 reports `total_scanned: 0` and skips `state.msgpack` as `SCAN_NOT_SUPPORTED`. |
|
|
| ## Evidence Summary |
|
|
| Artifact: |
|
|
| ```text |
| SHA256: 3ddf739096ea87558f341e1705b607510e7e7f3af4c37841b51bd8809b52e465 |
| Size: 506 bytes |
| ``` |
|
|
| Runtime: |
|
|
| ```json |
| "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: |
|
|
| ```json |
| "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.msgpack` restore and `try_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 `.msgpack` artifact, 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 `.msgpack` model artifacts that recursively detects nested pickle payloads in `msgpack-numpy` object-array records. |
|
|