Skip to content

fix(sharding): validate nested sharding codecs and reject an inner chunk size of 0 - #4480

Open
d-v-b wants to merge 3 commits into
zarr-developers:mainfrom
d-v-b:fix/sharding-inner-chunk-validation
Open

d-v-b wants to merge 3 commits into
zarr-developers:mainfrom
d-v-b:fix/sharding-inner-chunk-validation

Conversation

@d-v-b

@d-v-b d-v-b commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

This PR fixes a bug in the sharding codec that allowed the sharding codec to contain a second sharding codec with an incompatible chunk shape. the sharding codec also accepted an inner chunk edge of 0, which is inane and rejected at construction now.

🤖 AI text below 🤖

A sharding codec nested in the codecs of another sharding codec is now validated as the outer one is: its inner chunk shape must divide the chunk it encodes. Such an array used to be created, and opened, without error, and then read back wrong data or its fill value. ShardingCodec now rejects an inner chunk size of 0 (or False) when it is constructed, with a ValueError that names the dimension, as other Zarr implementations do; it used to accept one, which raised a ZeroDivisionError when an array was created or opened with the codec or, where the codec was nested in another sharding codec, on the first write to the array.

Closes #4437.

Problem

Found by an agent audit of chunk handling on main that followed #4374, looking for other places where an invalid chunk declaration is accepted, stored, and only fails (or misreads) later.

ShardingCodec.validate checks its own chunk_shape against the array's chunk grid, but never validated the codecs that encode each inner chunk. On main:

import numpy as np, zarr
from zarr.codecs import ShardingCodec
from zarr.storage import MemoryStore

data = np.arange(1, 121, dtype="i4").reshape(6, 20)
arr = zarr.create_array(
    MemoryStore(), shape=(6, 20), dtype="i4", chunks=(6, 20),
    serializer=ShardingCodec(chunk_shape=(6, 20), codecs=[ShardingCodec(chunk_shape=(4, 7))]),
)
arr[:] = data
np.array_equal(arr[:], data)  # False: 64 of 120 cells read back with wrong values

With a nested chunk_shape=(12, 40) everything reads back as the fill value, and with (0, 5) the array is created, [0, 5] is stored, the array reopens, and the first write raises ZeroDivisionError. The same shapes given to a top-level sharding codec were already rejected ((0, 5) with a bare ZeroDivisionError).

Changes

  • ShardingCodec.validate validates its inner codecs the way ArrayV3Metadata validates an array's codecs: each one against the chunk the codecs before it leave it (so permuted after a transpose, and with the data type a cast leaves), as a regular grid of one inner chunk. A nested sharding codec is thereby checked against the chunk it splits, at create and at open.
  • ShardingCodec.__init__ and __setstate__ parse the inner chunk shape as a shape whose every size is a chunk edge length (parse_chunk_shape(parse_shapelike(...))), so 0 and False are rejected. NumPy integers, a bare integer and True are accepted as before.

tensorstore (jb::Array(jb::Integer<Index>(1)) in the sharding_indexed binder) and zarrs (ChunkShape = Vec<NonZeroU64>) both reject a 0 when they parse the codec configuration.

Behaviour changes to review

Tests

  • test_nested_sharding_roundtrip: valid nested sharding (dividing, equal, after a transpose, three levels) is still accepted and round-trips.
  • test_nested_sharding_rejects_indivisible_inner_chunk_shape: rejected at create, store untouched.
  • test_open_rejects_stored_nested_sharding_with_indivisible_inner_chunk_shape: a stored document is rejected when parsed.
  • test_sharding_codec_rejects_inner_chunk_size_zero: constructor rejection.

The full suite and prek run --all-files pass locally.

Relation to other open PRs

This PR is a small stopgap for the data corruption reported in #4437. Two larger open PRs overlap with it:

Not fixed here, and left to #4352: a top-level sharding codec placed after a transpose is still checked against the untransposed chunk grid.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

TODO

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/user-guide/*.md
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

🤖 Generated with Claude Code

d-v-b added 3 commits October 5, 2026 17:00
…unk size of 0

`ShardingCodec.validate` checked its own inner chunk shape against the chunk
grid but never validated the codecs that encode each inner chunk. A sharding
codec nested in another one could therefore declare an inner chunk shape that
does not divide the chunk it splits: the array was created and opened without
error, and then read back wrong data or its fill value.

`validate` now validates the inner codecs as array metadata validates its
codecs: each against the chunk the codecs before it leave it (so permuted
after a transpose), as a regular grid of one inner chunk.

`ShardingCodec` also rejects an inner chunk size of 0 (or `False`) when it is
constructed or unpickled, as tensorstore and zarrs do when they parse the
codec configuration. It used to accept one, which raised a
`ZeroDivisionError` when an array used the codec or, where the codec was
nested, on the first write.

Assisted-by: ClaudeCode:claude-fable-5.1
@codecov

codecov Bot commented Oct 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.69%. Comparing base (a53d51f) to head (d2592d2).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4480   +/-   ##
=======================================
  Coverage   94.69%   94.69%           
=======================================
  Files          94       94           
  Lines       13598    13606    +8     
=======================================
+ Hits        12876    12884    +8     
  Misses        722      722           
Files with missing lines Coverage Δ
src/zarr/codecs/sharding.py 95.78% <100.00%> (+0.05%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@d-v-b
d-v-b marked this pull request as ready for review October 5, 2026 15:51
@d-v-b d-v-b added this to the 3.4.1 milestone Oct 5, 2026
@d-v-b

d-v-b commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

it's an important bugfix so I'm merging it. this change overlaps somewhat with the more comprehensive changes in #4352, but that's slated for 3.5.0

This branch has not been deployed

No deployments
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.

A sharding codec inside another is never validated, and an inner chunk shape that does not divide corrupts data silently

1 participant