YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
orbax-checkpoint 0.12.1: uncontrolled recursion in msgpack TUPLE ext-type handler causes SIGSEGV (native stack overflow) when loading a <1KB malicious checkpoint file
- Target:
orbax-checkpoint(PyPI), version 0.12.1 β confirmed current latest release at time of testing (pip index versions orbax-checkpointβ0.12.1, matches installed version). - Tested with: flax 0.12.7 + orbax-checkpoint 0.12.1, pip-installed in an isolated venv, Python 3.13.
- Category: Model File Format β native memory-safety crash (uncontrolled recursion β stack overflow β SIGSEGV) via a malicious checkpoint file.
- Related huntr program target: "Flax-second-bug".
Root cause
File: orbax/checkpoint/msgpack_utils.py
class _MsgpackExtType(enum.IntEnum):
NDARRAY = 1
NATIVE_COMPLEX = 2
NPSCALAR = 3
TUPLE = 4
def _msgpack_ext_unpack(code, data):
...
elif code == _MsgpackExtType.TUPLE:
return tuple(msgpack.unpackb(data, raw=False, ext_hook=_msgpack_ext_unpack)) # line 115
Each nested tuple, encoded as ExtType(4, ...), forces a brand-new, separate msgpack.unpackb() call from inside the ext_hook callback of the outer call. This chain of nested Python/C call-stack frames is not subject to msgpack's own internal container-nesting depth guard β that guard only bounds recursion within a single unpackb call over native array/map/str containers (confirmed separately: a raw 5000-level nested map triggers msgpack's own clean, catchable StackError, not a crash).
Because the TUPLE-ext recursion instead re-enters the C extension stack frame-by-frame across independent calls, it blows the real OS thread/C stack directly, causing a segmentation fault β well before Python's own sys.getrecursionlimit() (default 1000) would raise a catchable RecursionError.
Reachability β real public API
This is reachable via the actual public, documented checkpoint-loading API:
orbax.checkpoint.aggregate_handlers.MsgpackHandler.deserialize(path)
which reads the raw bytes of a legacy "aggregate" checkpoint file straight from disk and passes them unmodified to msgpack_utils.msgpack_restore() β msgpack.unpackb(data, ext_hook=_msgpack_ext_unpack, raw=False). Simply loading an attacker-supplied checkpoint file crashes the victim process β no special API misuse required.
Dedup note
This is a genuinely separate vulnerability from the already-filed flax.serialization msgpack_restore() / _unchunk() unbounded-chunk-allocation DoS:
| This finding | Prior flax finding | |
|---|---|---|
| Package | orbax-checkpoint |
flax (serialization module) |
| Vuln class | Native stack-overflow segfault via uncontrolled recursion | Unbounded heap allocation via np.concatenate |
| Function | _msgpack_ext_unpack's TUPLE-ext handling |
_unchunk |
| Ext type | Only exists in orbax's derived/extended copy of the msgpack module β flax.serialization does not define a TUPLE ext type at all (only ndarray/native_complex/npscalar) |
N/A |
PoC
A small generator (orbax_gen_payload.py) constructs N levels of nested msgpack ExtType(code=4 / TUPLE) wrappers by hand β pure msgpack.ExtType/packb calls, no orbax code needed to author the malicious file, only to load it. Each level's ext payload is msgpack.packb([inner_ext_or_value]).
python3 orbax_gen_payload.py <N> <outfile.msgpack>
Victim scripts included:
orbax_restore_victim.py <path>β calls the low-levelorbax.checkpoint.msgpack_utils.msgpack_restore()directly.orbax_full_path_victim.py <path>β calls the real, end-to-end public APIorbax.checkpoint.aggregate_handlers.MsgpackHandler().deserialize(path)(viaetils.epath.Path).
Negative control (N=190, <1KB payload)
$ python3 orbax_restore_victim.py nested_190_negative_control.msgpack; echo $?
RESTORED OK, nesting depth measured: 190
0
Both msgpack_restore() and the full public MsgpackHandler.deserialize() API restore successfully, exit code 0.
Positive / attack (N=200, 937-byte file)
$ python3 -X faulthandler orbax_full_path_victim.py nested_200_positive.msgpack
Fatal Python error: Segmentation fault
Current thread 0x00007fb92b7cd200 (most recent call first):
File ".../venv/lib/python3.13/site-packages/orbax/checkpoint/msgpack_utils.py", line 115 in _msgpack_ext_unpack
File ".../venv/lib/python3.13/site-packages/orbax/checkpoint/msgpack_utils.py", line 115 in _msgpack_ext_unpack
File ".../venv/lib/python3.13/site-packages/orbax/checkpoint/msgpack_utils.py", line 115 in _msgpack_ext_unpack
[... repeated identical frames ...]
Extension modules: jaxlib.cpu_feature_guard, numpy._core._multiarray_umath, ..., msgpack._cmsgpack (total: 16)
$ echo $?
139
Both the low-level msgpack_utils.msgpack_restore(data) call and the real end-to-end orbax.checkpoint.aggregate_handlers.MsgpackHandler().deserialize(path) checkpoint-loading call crash the interpreter with SIGSEGV (process exit code 139), reproduced deterministically across repeated runs (2/2 β see evidence_run1_low.log, evidence_run2_low.log, and evidence_run_full_faulthandler.log for the full captured faulthandler output, including repeated stack frames pinned at msgpack_utils.py:115 in _msgpack_ext_unpack).
Threshold sweep
| N (nesting levels) | file size | exit code |
|---|---|---|
| 160 | β | 0 |
| 170 | β | 0 |
| 180 | β | 0 |
| 190 | 887 B | 0 |
| 200 | 937 B | 139 (SIGSEGV) |
Environment confirmation
$ pip show orbax-checkpoint | head -2
Name: orbax-checkpoint
Version: 0.12.1
$ pip index versions orbax-checkpoint | head -1
orbax-checkpoint (0.12.1) # matches installed version = current PyPI release
$ ulimit -s
8192
$ python3 -c "import sys; print(sys.getrecursionlimit())"
1000
Verified this crash occurs below Python's own default recursion limit (sys.getrecursionlimit() == 1000) and with a standard/default OS stack ulimit (ulimit -s == 8192 KB, not artificially lowered) β ruling out a simple catchable RecursionError and confirming a genuine native memory-safety crash.
Files in this repo
orbax_gen_payload.pyβ malicious-checkpoint generator (no orbax dependency).orbax_restore_victim.pyβ low-level victim script (msgpack_utils.msgpack_restore).orbax_full_path_victim.py/victim_full.py,victim_low.pyβ real public-API victim scripts.nested_190_negative_control.msgpackβ 887-byte benign file, restores successfully (negative control).nested_200_positive.msgpackβ 937-byte malicious file, triggers SIGSEGV (positive/attack case).evidence_run_full_faulthandler.log,evidence_run1_low.log,evidence_run2_low.logβ captured verbatimfaulthandlercrash output from repeated runs.
Impact
Any consumer of orbax-checkpoint's legacy aggregate-checkpoint / msgpack loading path (directly, or transitively via flax checkpoint restore helpers that delegate to it) can be crashed by an attacker-supplied checkpoint file under 1KB in size β a straightforward denial-of-service against any service or pipeline that loads checkpoints from an untrusted or shared source (e.g. a model hub, shared storage, or a checkpoint accepted from a third party).