Skip to content

fix(fletcher32): reject an out buffer of the wrong size - #896

Open
Duo-ee wants to merge 1 commit into
zarr-developers:mainfrom
Duo-ee:fix/fletcher32-out-size
Open

Duo-ee wants to merge 1 commit into
zarr-developers:mainfrom
Duo-ee:fix/fletcher32-out-size

Conversation

@Duo-ee

@Duo-ee Duo-ee commented Nov 12, 2026

Copy link
Copy Markdown

Problem

Fletcher32.decode copies the payload into a caller-provided out buffer
without validating the destination size:

out_mv = ensure_contiguous_ndarray(out).view("uint8")
out_ptr = &out_mv[0]
memcpy(out_ptr, b_ptr, b_len - FOOTER_LENGTH)

Two failure modes:

  • out smaller than the decoded payload: out-of-bounds write past the end of
    the destination (heap corruption / crash).
  • out larger than the decoded payload: the tail of out is left untouched,
    so stale/uninitialized data is silently returned to the caller.

Fix

Validate the destination size before copying:

payload_len = b_len - FOOTER_LENGTH
if out_mv.size != payload_len:
    raise ValueError(
        f"Output buffer has {out_mv.size} bytes, but the"
        f" decoded payload has {payload_len} bytes."
    )
if payload_len:
    memcpy(&out_mv[0], b_ptr, payload_len)

out_mv is a 1-D uint8 view, so its element count equals the number of bytes.
The if payload_len: guard also keeps a zero-length payload from indexing
&out_mv[0] on an empty buffer.

Tests

Added to tests/test_fletcher32.py:

  • test_out_too_large: oversized out raises ValueError and is not written to
    (checked by filling it with a sentinel first).
  • test_out_too_small: undersized out raises ValueError instead of overflowing.
  • test_out_correct_size: exact-size out is filled and returned.
  • test_out_bytearray: a bytearray destination is filled correctly.

Verification

  • Cython 3.2.9 translates the patched fletcher32.pyx cleanly.
  • Built the patched module with the system MinGW gcc into a CPython 3.10
    extension, placed it in a temporary copy of the installed package, and ran the
    updated tests/test_fletcher32.py: 10 passed.
  • The same tests against an unpatched main build: 2 failed, 8 passed.
    test_out_too_large and test_out_too_small both fail with
    DID NOT RAISE ValueError, confirming that the new tests catch the bug.
  • ruff check tests/test_fletcher32.py and
    ruff format --check tests/test_fletcher32.py pass. ruff cannot parse
    .pyx files, so the Cython source is verified by compiling it rather than by
    linting it.
  • I did not run the full meson build locally: this Windows machine has no
    MSVC/meson toolchain, and the available Python 3.10 environment (with the
    matching installed package) predates the project's requires-python >= 3.12.
    The standalone module build above exercises the changed code path because
    fletcher32.pyx has no NumPy C-API dependency and the _utils helpers are
    cdef inline.

Relation to other PRs

Checklist

  • Unit tests added
  • Tests pass locally (standalone module build, see above)
  • Docstrings/API docs: no new user-facing API
  • Changes documented in docs/release.md
  • Docs build locally (not run)
  • GitHub Actions CI passes (to be checked after this PR runs)
  • Test coverage to 100% (to be checked by Codecov)

Fletcher32.decode copied the payload into a caller-supplied 'out' buffer
without checking its size: a smaller buffer led to an out-of-bounds write,
while a larger buffer left its tail untouched (stale data). Validate the
destination size before the copy and raise ValueError with the expected
payload size instead. The zero-length payload case now skips the memcpy
entirely, so an empty buffer is never indexed.

Add tests for oversized, undersized, exact-size and bytearray destinations.
@Duo-ee

Duo-ee commented Nov 12, 2026

Copy link
Copy Markdown
Author

A note on the red CI matrix so it is not mistaken for a regression from this change.

The build (3.12|3.13|3.14, ...) jobs fail during test collection, before any Fletcher32 test runs:

ERROR collecting tests/common.py
E   ValueError: invalid literal for int() with base 10: 'post0'

This comes from zfpy 1.0.1.post0 tripping the version parsing in tests/common.py, so 57 test modules fail to import. It is unrelated to this PR and reproduces on main and on other open PRs.

Where the matrix is not affected by that dependency issue, the change is fully exercised by the real meson build and the full test suite:

  • build (3.15, ubuntu-latest): tests/test_fletcher32.py — test_out_too_large, test_out_too_small, test_out_correct_size, test_out_bytearray all PASSED; whole suite 736 passed, 27 skipped
  • build (3.15, windows-latest): same four PASSED; 735 passed, 27 skipped, 1 xfailed
  • build (3.14t, macos-15-large): same four PASSED; 738 passed, 25 skipped

The "Install numcodecs" step (which runs Cython + meson for fletcher32.pyx) succeeds on every platform in the matrix, including the Python 3.12–3.14 jobs.

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