Skip to content

Hardening PyDataset, Callbacks, and Preprocessing adapt handling across Keras 3 - #23440

Open
MrNagabhushana-0-dev wants to merge 9 commits into
keras-team:masterfrom
MrNagabhushana-0-dev:megagroup-pydataset-callbacks-preprocessing-fix
Open

Hardening PyDataset, Callbacks, and Preprocessing adapt handling across Keras 3#23440
MrNagabhushana-0-dev wants to merge 9 commits into
keras-team:masterfrom
MrNagabhushana-0-dev:megagroup-pydataset-callbacks-preprocessing-fix

Conversation

@MrNagabhushana-0-dev

Copy link
Copy Markdown
Contributor

Description

Summary

This PR standardizes dataset adaptation handling and hardens callbacks and image utilities across Keras 3:

  1. PyDataset Adaptation in IndexLookup & TextVectorization:

    • Updated IndexLookup.adapt() and TextVectorization.adapt() to extract feature data from (x, y) and (x, y, sample_weight) tuple/list batches yielded by custom PyDataset data generators.
    • Preserves 1D string list batches without misclassifying them as target tuples.
  2. PyDataset.__init__ Parameter Forwarding:

    • Added **kwargs support to PyDataset.__init__() signature to allow custom subclasses to safely pass initializers to super().__init__().
  3. ModelCheckpoint Path Safety:

    • Added a relative path traversal (..) safeguard warning when saving model checkpoints to custom output file paths.
  4. load_img Oversized Image Protection:

    • Added explicit handling for PIL.Image.DecompressionBombError in load_img(), converting raw PIL errors into clear, actionable ValueError exceptions.

Verification & Tests

  • Added and updated tests in index_lookup_test.py, text_vectorization_test.py, py_dataset_adapter_test.py, model_checkpoint_test.py, and image_utils_test.py.
  • Full verification passed Locally : 117 passed, 5 skipped (0 failures).

Contributor Agreement

Please review our AI-Assisted Contribution Policy and check all boxes below before submitting your PR for review:

  • I am a human, and not a bot.
  • I will be responsible for responding to review comments in a timely manner.
  • I will work with the maintainers to push this PR forward until submission.

~ > Homosapien Here :)

Nagabhushanaraju and others added 8 commits July 5, 2026 01:27
Fixes issue where invalid class_weight keys were silently ignored in
model.fit(), causing sample_weight to default to 1.0 for all samples.
Now raises a ValueError with a clear message when a key cannot be
converted to an integer class index.

Fixes keras-team#23220
Fixes issue where invalid class_weight keys were silently ignored in
model.fit(), causing sample_weight to default to 1.0 for all samples.
Now raises a ValueError with a clear message when a key cannot be
converted to an integer class index.

Contributed by Nagabhushana — Fixes keras-team#23220 (Patch Complete)
Hopefully this finally resolves the 'files were modified by this hook' error
in the api-gen CI step. The api_gen.py file has now been fully formatted so the hook
should see a perfectly clean working tree after it runs.

If there are more hidden validation layers, one can only wonder how many errors
are hiding like it. Bro, work already!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces relative path traversal warnings in ModelCheckpoint, extracts inputs from tuple/list batches during preprocessing layer adaptation, and improves error handling for PIL's DecompressionBombError in image_utils.py. The review highlights critical issues: the path traversal check is bypassed because os.path.normpath resolves relative segments before the check; _extract_batch incorrectly discards elements when batches are plain 1D lists; and DecompressionBombError is not caught because Image.open() is lazy and needs an explicit img.load() call within the try block.

Comment thread keras/src/callbacks/model_checkpoint.py Outdated
Comment on lines +381 to +387
normalized_path = os.path.normpath(file_path)
if ".." in normalized_path.split(os.sep):
warnings.warn(
f"ModelCheckpoint filepath '{file_path}' contains relative path "
"traversal ('..'). Ensure checkpoint directory paths are safe.",
stacklevel=2,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The path traversal check is performed on normalized_path after calling os.path.normpath(file_path). Since os.path.normpath resolves relative path segments like .., any traversal sequence (e.g., /safe/dir/../../etc/passwd) will be fully resolved to its target path (e.g., /etc/passwd) before the check is executed. As a result, ".." will not be present in the split segments of the normalized path, completely bypassing the safeguard.\n\nTo fix this, the check should be performed on the original file_path before normalization to ensure any relative path traversal attempts are correctly detected.

        if ".." in file_path.replace("/", os.sep).split(os.sep):
            warnings.warn(
                f"ModelCheckpoint filepath '{file_path}' contains relative path "
                "traversal ('..'). Ensure checkpoint directory paths are safe.",
                stacklevel=2,
            )

Comment on lines +15 to +19
def _extract_batch(batch):
"""Return input from batch; handle (x, y) or (x, y, sample_weight)."""
if isinstance(batch, (tuple, list)):
return batch[0]
return batch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of _extract_batch always returns batch[0] if the batch is a list or tuple. While this correctly extracts the input x from (x, y) or (x, y, sample_weight) tuples, it is completely broken when a custom PyDataset yields a plain 1D list of inputs (e.g., ["apple", "banana"] or [1, 2]). In those cases, _extract_batch will return only the first element (e.g., "apple" or 1), discarding the rest of the batch and leading to incorrect adaptation.\n\nTo fix this, we should check if the first element of the batch is a container (list, tuple, numpy array) or a tensor. If it is, we treat it as a (x, y) structure and return batch[0]. Otherwise, we return the entire batch.

def _extract_batch(batch):
    """Return input from batch; handle (x, y) or (x, y, sample_weight)."""
    if isinstance(batch, (tuple, list)) and len(batch) in (2, 3):
        if (
            isinstance(batch[0], (list, tuple, np.ndarray))
            or tf.is_tensor(batch[0])
            or backend.is_tensor(batch[0])
        ):
            return batch[0]
    return batch

Comment on lines +242 to +263
try:
if isinstance(path, io.BytesIO):
img = pil_image.open(path)
elif isinstance(path, (pathlib.Path, bytes, str)):
if isinstance(path, pathlib.Path):
path = str(path.resolve())
with open(path, "rb") as f:
img = pil_image.open(io.BytesIO(f.read()))
else:
raise TypeError(
"path must be a string, bytes, pathlib.Path, or BytesIO. "
f"Received path={path} (of type {type(path)})"
)
except Exception as e:
if hasattr(pil_image, "DecompressionBombError") and isinstance(
e, pil_image.DecompressionBombError
):
raise ValueError(
f"Image load failed for path '{path}' due to PIL DecompressionBombError. "
"The image exceeds PIL's maximum pixel limit."
) from e
raise e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Pillow's Image.open() is lazy and only reads the image headers without decompressing the pixel data. The actual decompression (which triggers DecompressionBombError for oversized images) is deferred until the image is processed or converted (e.g., via img.convert() or img.resize() later in the function).\n\nBecause those processing steps occur outside of this try...except block, any DecompressionBombError will not be caught here and will propagate as an unhandled raw PIL error, bypassing the intended safeguard.\n\nTo fix this, we should explicitly call img.load() inside the try block to force immediate decompression and ensure any decompression bomb errors are caught and handled correctly.

    try:
        if isinstance(path, io.BytesIO):
            img = pil_image.open(path)
        elif isinstance(path, (pathlib.Path, bytes, str)):
            if isinstance(path, pathlib.Path):
                path = str(path.resolve())
            with open(path, "rb") as f:
                img = pil_image.open(io.BytesIO(f.read()))
        else:
            raise TypeError(
                "path must be a string, bytes, pathlib.Path, or BytesIO. "
                f"Received path={path} (of type {type(path)})"
            )
        img.load()
    except Exception as e:
        if hasattr(pil_image, "DecompressionBombError") and isinstance(
            e, pil_image.DecompressionBombError
        ):
            raise ValueError(
                f"Image load failed for path '{path}' due to PIL DecompressionBombError. "
                "The image exceeds PIL's maximum pixel limit."
            ) from e
        raise e

@codecov-commenter

codecov-commenter commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.25000% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.15%. Comparing base (9ac86f1) to head (20e8256).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
keras/src/utils/image_utils.py 28.57% 7 Missing and 3 partials ⚠️
keras/src/callbacks/model_checkpoint.py 0.00% 1 Missing and 1 partial ⚠️
...ras/src/layers/preprocessing/text_vectorization.py 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23440      +/-   ##
==========================================
- Coverage   84.90%   84.15%   -0.75%     
==========================================
  Files         468      468              
  Lines       70724    70732       +8     
  Branches    11715    11718       +3     
==========================================
- Hits        60047    59527     -520     
- Misses       7667     8204     +537     
+ Partials     3010     3001       -9     
Flag Coverage Δ
keras 83.98% <56.25%> (-0.73%) ⬇️
keras-cpu 83.98% <56.25%> (+<0.01%) ⬆️
keras-gpu ?
keras-jax 58.08% <56.25%> (-0.32%) ⬇️
keras-numpy 53.91% <50.00%> (+0.02%) ⬆️
keras-openvino 59.63% <56.25%> (+0.02%) ⬆️
keras-tensorflow 59.72% <56.25%> (-0.29%) ⬇️
keras-torch 59.13% <56.25%> (-0.37%) ⬇️
keras-tpu ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants