Instructions to use EnigmaConsultant/huntr-poc-keras-buildconfig-shape-alloc-oom with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use EnigmaConsultant/huntr-poc-keras-buildconfig-shape-alloc-oom with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://EnigmaConsultant/huntr-poc-keras-buildconfig-shape-alloc-oom") - Notebooks
- Google Colab
- Kaggle
YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Keras 3 .keras loader: uncontrolled memory allocation via attacker-controlled build_config layer shapes
Target: keras (PyPI) β verified against keras 3.15.0, numpy backend
Public entrypoint: keras.saving.load_model(path, safe_mode=True, compile=False)
Class: CWE-789 (Memory Allocation with Excessive Size Value) / CWE-409 (Improper Handling of Highly Compressed Data β decompression/expansion bomb)
Impact: Denial of Service β a ~1.4 KB .keras model file with no large data forces an arbitrarily large host-side memory allocation (OOM-kill / MemoryError). safe_mode=True provides no protection.
Root cause
When loading a .keras v3 archive, serialization_lib.deserialize_keras_object() reconstructs
each layer and then, at serialization_lib.py:785-787, unconditionally rebuilds it from the
embedded build_config with no validation of the shapes it declares:
build_config = config.get("build_config", None)
if build_config and not instance.built:
instance.build_from_config(build_config)
For a Dense layer, Dense.build() (dense.py:136) issues:
self._kernel = self.add_weight(
name="kernel",
shape=(input_shape[-1], self.units),
initializer=self.kernel_initializer, # GlorotUniform
...
)
which materializes the kernel array via the initializer. Both integers that determine the
kernel shape are attacker-controlled JSON fields in the tiny config.json:
build_config["input_shape"][-1]- the layer config's
units
The numpy initializer path (initializers/random_initializers.py:316 β backend/numpy/random.py:23 uniform)
allocates the array in float64, so the peak resident spike is 8Γ the nominal float32 weight size.
Crucially, the allocation is fabricated purely from config.json integers and is unrelated to the
actual weight data present in the archive. In this PoC the archive's model.weights.h5 still holds
only a benign (4, 3) kernel, yet the loader tries to allocate a (units, input_shape[-1]) array
before it ever reaches the weight-shape check. A weaponized file is therefore ~1.4 KB and carries no
large data of its own.
safe_mode=True gives no protection here β it only guards the __lambda__ special-case in the
deserializer, not the build_from_config shape path.
Verified root-cause traceback (evil_huge, verbatim frames)
saving_lib.py:434 _model_from_config
serialization_lib.py:775 deserialize_keras_object (instance = cls.from_config(inner_config))
sequential.py:393 from_config
serialization_lib.py:787 deserialize_keras_object (instance.build_from_config(build_config))
layer.py:491 build_from_config
dense.py:136 build
layer.py:622 add_weight
variables.py:418 _initialize_with_initializer
random_initializers.py:316 __call__
backend/numpy/random.py:23 uniform
-> numpy._core._exceptions._ArrayMemoryError:
Unable to allocate 7.28 TiB for an array with shape (1000000, 1000000) and data type float64
PoC
Built a genuine 1-layer Sequential model Input((4,)) -> Dense(3) and saved it with
keras.Model.save() β base.keras (11,580 B). Then, keeping the identical valid
metadata.json and the original tiny model.weights.h5, edited two integers in config.json
β the Dense layer's units and build_config.input_shape[-1] β and repackaged as a valid ZIP.
Two weaponized variants:
| file | size | units |
input_shape |
|---|---|---|---|
evil_mod.keras |
1,415 B | 20000 | [None, 20000] |
evil_huge.keras |
1,414 B | 1000000 | [None, 1000000] |
Each is loaded via the public API keras.saving.load_model(path, safe_mode=True, compile=False)
against the real installed keras 3.15.0 (numpy backend) in a clean venv. Negative control: the
untampered base.keras.
Artifacts in this repo: base.keras, evil_mod.keras, evil_huge.keras, build_and_verify.py
(builds the evil files from base and runs all three with RSS measurement).
Reproduce
python -m venv venv && . venv/bin/activate
pip install "keras==3.15.0" numpy
python build_and_verify.py
Captured evidence (verbatim)
keras 3.15.0 backend numpy
baseline rss after import: 255 MB
[CONTROL] base.keras 11580B -> loaded OK, peak rss 257 MB
[EVIL-MOD] evil_mod.keras 1415B -> ValueError AFTER alloc; peak rss 4835 MB (delta ~4578 MB from a 1415-byte file)
[EVIL-HUGE] evil_huge.keras 1414B -> MemoryError: Unable to allocate 7.28 TiB for an array
with shape (1000000, 1000000) and data type float64
evil_modforces a ~4.6 GB resident spike from a 1.4 KB file. The allocation completes before the later weight-shapeValueErroris raised β i.e. the DoS lands even though the file is ultimately "invalid".evil_hugeattempts a 7.28 TiB allocation β numpyMemoryError.
Negative controls
- (a)
base.keras(untampered, 11,580 B) loads to a 257 MB peak β no spike. - (b) A deeply-nested config raises a catchable
RecursionError, not this bug β confirming the vector is the build-shape path, not generic nesting. - (c) CPython
json.loadson 1e6-deep nesting raisesRecursionError(no segfault) β confirming this finding is thebuild_from_configshape path, not a JSON-parser issue.
Dedup note
Distinct from prior Keras DoS findings in this account:
huntr-poc-dos-keras-config-json-bomb-4gib-floorβ abuses rawconfig.jsonsize / JSON parsing volume. This finding uses a tiny (~1.4 KB) config and instead abuses semantic integer fields (units,input_shape) consumed bybuild_from_config; no large data present.huntr-poc-keras-legacy-h5-weightnames-stackbomb-oomβ legacy.h5weight-name path. This is the v3.kerasbuild_configpath (serialization_lib.py:787βDense.build), a different sink.huntr-poc-keras-assets-zipbombβ ZIP decompression of assets. This finding is not a compression bomb of the archive; the allocation is fabricated from config integers independent of any stored/compressed bytes.huntr-poc-keras-nested-lambda/ lambda RCE β unrelated__lambda__code-exec path.
No known CVE covers build_config shape-driven allocation in the Keras 3 v3 loader at time of
filing. The vector survives safe_mode=True.
- Downloads last month
- -