Honor safe_mode on the legacy/.h5 path and restrict Lambda function-gadget module resolution - #910
Conversation
…ule gadgets `load_model(..., safe_mode=True)` is documented to disallow unsafe `lambda` deserialization, but two paths bypass it: 1. `_retrieve_class_or_fn` imports an arbitrary module named in a `Lambda` layer's `function` config (`function_type=="function"`) and fetches a symbol from it, with no restriction -- so a config naming `os`/`subprocess`/ `builtins` resolves an arbitrary callable even under `safe_mode=True`. 2. The legacy (`.h5`/SavedModel) load path never establishes a `SafeModeScope`, so `in_safe_mode()` defaults to falsy and unsafe `Lambda` bytecode is loaded even when `safe_mode=True`. Restrict module resolution to TF-Keras/TensorFlow modules under safe mode, and wrap the legacy load in a `SafeModeScope(safe_mode)` so the existing unsafe- lambda guard fires. Add a test for the gadget-module case.
There was a problem hiding this comment.
Code Review
This pull request ensures that safe_mode is honored in legacy model loading paths by wrapping legacy load_model calls in a SafeModeScope. Additionally, it restricts module resolution under safe mode to allowed TF-Keras, TensorFlow, and NumPy modules to prevent arbitrary code execution vectors. A corresponding unit test has been added to verify this behavior. There are no review comments, and I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
@hertschuh could you take a look when you get a chance? On the legacy .h5 path, safe_mode isn't honored and Lambda layers can resolve arbitrary modules as function gadgets, which reopens the deserialization concern safe_mode is meant to close. The change honors safe_mode there and restricts the module resolution. Glad to adjust the allowlist if there are legit modules I've missed. |
Summary
load_model(..., safe_mode=True)(the default) is documented to "disallow unsafelambdadeserialization", but two paths do not honor it. This brings tf-keras in line with thesafe_modehardening that landed in Keras 3 (which had the same gaps).1. Arbitrary module/gadget resolution under
safe_modeFor a
Lambdalayer whosefunctionconfig isfunction_type=="function",_retrieve_class_or_fn(serialization_lib.py) imports the module named in the config and fetches a symbol from it with no restriction — so a config namingos/subprocess/builtinsresolves an arbitrary callable (e.g.os.system) and installs it as the layer'sfunction, even undersafe_mode=True. Only thefunction_type=="lambda"path was guarded.Fix: under safe mode, restrict the module-import fall-through to TF-Keras / TensorFlow modules (
keras,tf_keras,tensorflow,numpy); legitimate built-in symbols are still resolved by the existing fast paths above. Anything else raises with the standardsafe_modemessage and thesafe_mode=Falseescape hatch.2. Legacy (
.h5/ SavedModel) load silently ignoressafe_modeload_model(path, safe_mode=True)dispatches.h5/.hdf5to the legacy loader without passing or scopingsafe_mode(saving_api.py). The legacy path therefore never enters aSafeModeScope,in_safe_mode()defaults to falsy, and unsafeLambdabytecode loads even whensafe_mode=True.Fix: wrap the legacy load in
SafeModeScope(safe_mode)so the existing unsafe-lambda guard fires on the.h5path too (matching the.keraspath's behavior).Tests
Added
test_function_gadget_blocked_in_safe_mode(gadget from a non-Keras module is rejected under safe mode, allowed undersafe_mode=False). Verified locally ontf-keras==2.21.0: with these changes, loading a crafted.h5Lambda model and resolving anos.systemgadget both raise under the defaultsafe_mode=True(previously both succeeded).Compatibility
safe_mode=Falsecontinues to allow all of the above (the documented opt-out). Legitimate Keras/TensorFlow Lambda functions resolve unchanged.