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.

Uncontrolled memory allocation via attacker-controlled Embedding input_dim in a .keras layer config during default load_model (CWE-789 / CWE-400 DoS)

Target

  • Project: Keras (keras on PyPI)
  • Version verified: keras 3.15.0 (numpy backend), Python 3.13
  • Vulnerable file/line: keras/src/layers/core/embedding.py:149
  • Load path: default keras.saving.load_model(path) with safe_mode=True, compile=True (the documented defaults)
  • Class: CWE-789 Memory Allocation with Excessive Size Value / CWE-400 Uncontrolled Resource Consumption (DoS)

Summary

Loading an untrusted .keras model file with the ordinary, fully-default keras.saving.load_model() call can be driven to an unbounded host-side memory allocation by a ~2.8 KB file. The amplifier is the Embedding layer's own scalar config field input_dim, which is echoed straight into a weight-tensor shape and materialized during model deserialization β€” before the caller ever runs, trains, or predicts.

safe_mode=True provides no protection here: safe_mode only guards __lambda__ (arbitrary-code) deserialization, not resource sizing.

Root cause

During .keras deserialization, serialization_lib.py:787 unconditionally calls instance.build_from_config(build_config) for any layer that carries a build_config. For an Embedding layer this reaches Embedding.build(), which computes the embeddings table shape purely from the layer's own scalar config fields and ignores input_shape entirely:

# keras/src/layers/core/embedding.py
def build(self, input_shape=None):
    if self.built:
        return
    embeddings_shape = (self.input_dim, self.output_dim)   # line 149 β€” attacker-controlled input_dim
    ...
    if self.quantization_mode not in ("int8", "int4"):
        self._embeddings = self.add_weight(
            shape=embeddings_shape,
            initializer=self.embeddings_initializer,
            name="embeddings",
            ...
        )

input_dim is read from the JSON config.json inside the .keras zip. It is only validated as a positive integer (> 0, int) β€” there is no upper bound and no cross-check against the tiny build_config.input_shape. It is a 64-bit JSON integer, so an attacker can set it arbitrarily large in a file that stays a few kilobytes.

add_weight() -> backend.Variable(...) -> _initialize_with_initializer -> the default RandomUniform initializer -> keras/src/backend/numpy/random.py:23:

return rng.uniform(size=shape, low=minval, high=maxval).astype(dtype)

NumPy's Generator.uniform allocates the array in float64 first and only then .astype(dtype) down to float32. That adds an ~8x resident spike over the nominal float32 weight size, making the DoS cheaper for the attacker (for shape (300000000, 8) the float64 buffer alone is 17.9 GiB).

Proof of concept

Three scripts (mk.py, craft.py, load.py) plus the artifacts (benign.keras, evil.keras) are included in this repo.

  1. mk.py β€” builds a benign functional model containing Embedding(input_dim=16, output_dim=8) and saves benign.keras (19962 B).
  2. craft.py β€” opens benign.keras, rewrites only config.json to set the Embedding layer config's input_dim = 300000000, and re-zips as evil.keras (2796 B). Nothing else is changed β€” in particular the layer's build_config.input_shape stays tiny at [None, 4].
  3. load.py β€” sets a 2 GB RLIMIT_AS cap and calls the fully-default keras.saving.load_model(path) (safe_mode=True, compile=True).

Captured evidence (verbatim, keras 3.15.0 / numpy backend)

########## NEGATIVE CONTROL (benign) ##########
=== default keras.saving.load_model('benign.keras') under 2GB RLIMIT_AS ===
keras 3.15.0 backend numpy
LOADED OK: Functional params: 194

########## EVIL (huge input_dim) ##########
  File ".../keras/src/layers/core/embedding.py", line 157, in build
    self._embeddings = self.add_weight(
  File ".../keras/src/layers/layer.py", line 622, in add_weight
    variable = backend.Variable(
  File ".../keras/src/backend/common/variables.py", line 210, in __init__
    self._initialize_with_initializer(initializer)
  File ".../keras/src/backend/common/variables.py", line 418, in _initialize_with_initializer
    initializer(self._shape, dtype=self._dtype)
  File ".../keras/src/initializers/random_initializers.py", line 187, in __call__
    return random.uniform(
  File ".../keras/src/backend/numpy/random.py", line 23, in uniform
    return rng.uniform(size=shape, low=minval, high=maxval).astype(dtype)
  File "numpy/random/_generator.pyx", line 1102, in numpy.random._generator.Generator.uniform
numpy._core._exceptions._ArrayMemoryError: Unable to allocate 17.9 GiB for an array with shape (300000000, 8) and data type float64
=== default keras.saving.load_model('evil.keras') under 2GB RLIMIT_AS ===
keras 3.15.0 backend numpy
MemoryError: MemoryError((300000000, 8), dtype('float64'))

Proof the amplifier is input_dim, not build_config.input_shape

Embedding layer config input_dim = 300000000
Embedding build_config = {'input_shape': [None, 4]}
evil.keras size = 2796 bytes

The tiny input_shape is untouched; the allocation is driven entirely by the scalar input_dim config field. The negative-control benign file loads normally under the same 2 GB cap.

Impact

A ~2.8 KB .keras file, loaded via the default load_model() API, forces a multi-gigabyte-to-unbounded host memory allocation and crashes the process with MemoryError before any user code runs. Any service or pipeline that deserializes user-supplied Keras models (model hubs, CI validators, inference-serving upload endpoints, notebooks) is exposed to a trivial denial-of-service. The float64-then-astype behavior of the default initializer multiplies the resident spike ~8x, lowering the attacker's cost further.

Suggested remediation

Validate Embedding.input_dim (and, generally, config-derived weight shapes reconstructed during build_from_config) against a sane bound and/or the model's declared input_shape before calling add_weight(); alternatively cap total allocatable weight elements during deserialization. Deserialization should never size a raw allocation directly from an unvalidated file-supplied scalar.

Dedup / novelty note

This is distinct from other Keras allocation findings in the same audit:

  • NOT Dense/build_config.input_shape or units (dense.py, "buildconfig-alloc") β€” here the tiny build_config.input_shape is deliberately left untouched; the amplifier is the Embedding-specific scalar input_dim in embedding.py.
  • NOT compile-time metric amplifiers AUC.num_thresholds or IoU.num_classes.
  • NOT the config.json zip/JSON size bomb, numpy dtype/subarray, or legacy H5 model-config paths.

No existing HuggingFace PoC repo under this account and no prior flip-list entry covers the Embedding input_dim allocation path. No known CVE for this specific parameter at time of writing.

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support