Skip to content

Reject zero and non-finite SCALE/OFFSET under unpack=True; guard pack=True against divide-by-zero (#3104)#3129

Merged
brendancol merged 2 commits into
mainfrom
deep-sweep-security-geotiff-2026-06-09
Jun 10, 2026
Merged

Reject zero and non-finite SCALE/OFFSET under unpack=True; guard pack=True against divide-by-zero (#3104)#3129
brendancol merged 2 commits into
mainfrom
deep-sweep-security-geotiff-2026-06-09

Conversation

@brendancol

@brendancol brendancol commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Closes #3104

  • _extract_scale_offset now raises MalformedScaleOffsetError when SCALE is zero or non-finite, or when OFFSET is non-finite. This is the same fail-closed handling geotiff: reject malformed SCALE/OFFSET under mask_and_scale #2992 gave unparseable values: a SCALE of 0 collapses every pixel to the offset on read and has no inverse for pack=True.
  • _pack refuses a zero or non-finite scale_factor / non-finite add_offset (reachable only via hand-edited attrs once the read side rejects them) instead of dividing by zero and writing a file where every pixel is the nodata sentinel.

Backend coverage: the fix lives in the one metadata helper every unpack path resolves through. Eager numpy and GPU reads call it via _finalize_eager_read; dask and dask+GPU call it in _backends/dask.py. The VRT path rejects unpack=True at the boundary and is unaffected. GPU and dask+GPU rejection is covered by requires_gpu-gated tests and was verified on a live CUDA host.

Test plan:

  • New tests in xrspatial/geotiff/tests/read/test_scale_zero_3104.py: SCALE 0/-0.0/nan/inf/-inf and OFFSET nan/inf rejection on numpy and dask, GPU and dask+GPU rejection plus a valid-scale GPU unpack (gated on requires_gpu), zero SCALE ignored without unpack, negative finite SCALE still honoured, pack guards for hand-edited attrs (28 cases on a CUDA host, 25 without)
  • xrspatial/geotiff/tests/read + tests/write: 2352 passed, 5 skipped
  • flake8 clean on changed files

Also carries the security-sweep state CSV update for the geotiff module (audit pass 2026-06-09).

@brendancol brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PR Review: Reject zero and non-finite SCALE/OFFSET under unpack=True; guard pack=True against divide-by-zero (#3104)

Blockers (must fix before merge)

None.

Suggestions (should fix, not blocking)

  • The PR body says unpack is "rejected at the boundary on GPU and VRT paths". True for VRT, stale for GPU: #3075 added unpack on the GPU and dask+GPU read paths. Both route through _extract_scale_offset (GPU eager via _finalize_eager_read in _attrs.py, dask+GPU via _backends/dask.py:434), so the fix already covers them. Confirmed with a live CUDA run: open_geotiff(path, unpack=True, gpu=True) and the chunks=2 variant both raise MalformedScaleOffsetError on a SCALE=0 file, and a valid SCALE=2 file still unpacks. Update the PR body and add a requires_gpu-gated case to test_scale_zero_3104.py so the GPU path stays pinned by CI on GPU hosts.

Nits (optional improvements)

  • The module docstring of test_scale_zero_3104.py names only the eager and dask paths. Mention the GPU paths once the gated test lands.

What looks good

  • The rejection lives in the one helper all read paths share, so there is no per-backend drift to maintain.
  • MalformedScaleOffsetError keeps the #2992 contract (a ValueError subclass), so existing except ValueError callers keep catching it.
  • Tests cover -0.0 (which compares equal to 0.0) and pin negative finite SCALE as still honoured, so legitimate inverted-scale files keep working.
  • The _pack guard names both attrs and the reason in its message, and the hand-edited-attrs route has its own tests.

Checklist

  • Algorithm matches reference (n/a: validation change)
  • Backends consistent: numpy/dask tested; GPU verified by hand, gated test requested above
  • NaN handling correct (non-finite values rejected up front)
  • Edge cases covered: 0, -0.0, nan, +/-inf, offset variants, no-unpack passthrough
  • Dask chunk boundaries: n/a, rejection happens at metadata time before graph build
  • No premature materialization (raises before any pixel work)
  • Benchmark not needed
  • README feature matrix not applicable
  • Docstrings updated

@brendancol brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up review of 85544b1 (GPU-gated tests added after the first pass):

  • Both suggestions are addressed: test_unpack_rejects_zero_scale_gpu covers GPU and dask+GPU through the requires_gpu marker (the suite's usable-CUDA probe, not a bare cupy import), test_valid_scale_still_unpacks_gpu pins the non-rejection side, and the module docstring now names all four read paths. The PR body's backend-coverage paragraph was corrected too.
  • The hasattr(arr, "get") guard in the valid-scale GPU test is right: small GPU reads can come back as host arrays, so an unconditional .get() would break.
  • Ran the file on this CUDA host: 28 passed. No new findings.

Disposition of the first-pass findings: both fixed in-PR, nothing deferred or dismissed.

@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 10, 2026
@brendancol brendancol merged commit 7ccec77 into main Jun 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unpack=True accepts SCALE=0 and non-finite SCALE/OFFSET; pack=True then divides by zero and writes a corrupt file

1 participant