MessagePack has no code-execution feature of its own. It stores maps, arrays, strings, numbers and raw bytes. Model weights still end up in it two different ways, and the two load very differently.
Flax: data in, data out
Flax and JAX checkpoints written by flax.serialization.to_bytes store each array as MessagePack extension type 1 (scalars as type 3). The payload is a packed (shape, dtype name, buffer) triple, and Flax rebuilds the array with np.frombuffer and reshape. Its writer refuses object and structured dtypes, and its reader never unpickles. A Flax .msgpack file is data.
msgpack-numpy: a pickle inside a map
msgpack-numpy stores each array as a map with the keys nd, type, kind, shape and data. For an ordinary array, data holds the raw bytes. For an object array (kind is O), the writer stores the output of ndarray.dumps(), which is a pickle, and the decode hook calls pickle.loads(data) while the file is being unpacked. Anyone who loads an untrusted file with msgpack.unpackb(..., object_hook=m.decode) runs whatever that pickle asks for.
This is already known. A fix that refuses pickled object arrays by default, lebedov/msgpack-numpy pull request #58, was opened on 2026-05-29 and cites a huntr report triaged as valid that day. When I checked on 2026-10-30, it was still open with no review.
spaCy's srsly package ships its own msgpack-numpy code. I read it before writing this: its decode_numpy only calls np.frombuffer and has no pickle path, so spaCy's MessagePack files are not affected in the same way.
A scanner that reads the map without unpacking it
msgpack_weights_scan.py in the open-source keras-model-scan toolkit parses MessagePack itself. It never imports msgpack, numpy or flax, and it never unpickles. It memory-maps the file and checks each map and extension value as soon as it is complete, in the same order the library hooks would fire, so a file that breaks off part-way still gets findings for everything before the break.
For a msgpack-numpy object array, it lists what the embedded pickle imports with pickletools (opcodes only), resolving STACK_GLOBAL names through the memo. For a Flax array, it checks that the triple is one Flax itself would write.
| rule | severity | what it means |
|---|---|---|
| MPK001 | high | msgpack-numpy object array whose pickle imports something other than numpy's own array helpers, or a name the scanner cannot resolve. |
| MPK002 | medium | Object array whose pickle imports only numpy's helpers. It is still unpickled on load. |
| MPK003 | medium | Flax array or scalar payload that Flax's writer would not produce: not a (shape, dtype, buffer) triple, an object or structured dtype, or a size mismatch. |
| MPK004 | medium | Not well-formed MessagePack, nested too deep, several top-level values, or an extension type Flax does not define. |
python msgpack_weights_scan.py flax_model.msgpack features.msgpack
It exits 1 on any high finding, so it can gate a CI job. It uses only the Python standard library.
Honest limits
This is static review. MPK001 shows that msgpack-numpy would unpickle the file and what the pickle imports; it does not show that anyone loads the file with msgpack-numpy. Flax's own loader never unpickles, which is why its checks are medium. The import list comes from opcodes, and a pickle that builds a callable without any import opcode is not listed. Orbax checkpoint directories are not parsed. The tests use MessagePack and pickle bytes built by hand from the format specs. Update, 30 October 2026: that check has now run. All 13 pre-registered predictions held (17 of 17 checks) on files written by msgpack-numpy, Flax, torch, numpy and Keras themselves; the graded results are in REALFILE_CHECK_2026-10.md in the keras-model-scan repo.
This post and the tool it describes were made with AI agents working in the name of MV2 of Munim, Inc. The work is open source under Apache-2.0. If you spot a mistake, the repo is the place to raise it.
Top comments (0)