Skip to content

BUG: Do fslio's size and offset arithmetic at 64-bit width - #73

Merged
hjmjohnson merged 2 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:fix/fslio-64bit-arithmetic
Sep 23, 2026
Merged

hjmjohnson merged 2 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:fix/fslio-64bit-arithmetic

Conversation

@gdevenyi

Copy link
Copy Markdown

fslio computes buffer sizes and file offsets from short dimensions and int word sizes, so the products are evaluated in int and truncated before they are assigned to the size_t that holds them.

volbytes = xdim * ydim * zdim * wordsize;   /* all int; wraps past 2GB */
offset   = ((ydim * zVox + yVox) * xdim + xVox) * wordsize;

FslGetVolSize() has the same shape. Each operand is now widened before the multiply, so the arithmetic happens at the width of the result.

Split out of #52, which is otherwise mechanical cast work to silence -Wshorten-64-to-32. This is the one real defect in that set, so it is worth reviewing on its own. #52 no longer contains it.

Note fsliolib/ is behind USE_FSL_CODE, which defaults to OFF, so the standard workflows do not compile it. Verified locally with -DUSE_FSL_CODE=ON; the test suite is unchanged.

🤖 Generated with Claude Code

@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto master b4876bf, commit message cleaned, and a red-green regression test added. 28 ancestor commits are already upstream under new SHAs and dropped out; the branch is now the one fix commit plus a test.

Message cleanup

Removed the Co-Authored-By: naming an AI tool and the Claude-Session: URL. Replaced the bare #22 cross-reference with words (this is a fork, so the number names a different PR upstream).

Corrected one stale claim: the body said the same defect was "already fixed in FslSeekVolume() in the previous commit". That commit is not in this branch and FslSeekVolume() is unfixed on master — see the follow-up note below. The body now also states the undefined-behavior nature of the overflow, which is what the test showed.

Test and red proof

Added fsliolib/fslio_volsize_test.c, registered as ctest fslio_volsize_64bit. It drives FslGetVolSize() with dimensions whose product exceeds INT_MAX and asserts the exact 64-bit value, plus a 64x64x32 case so an over-broad change cannot pass it.

With the FslGetVolSize() hunk reverted, in a -DCMAKE_BUILD_TYPE=Debug build:

363: FAILURE: 2048x2048x1024: got 0, expected 4294967296
363: FAILURE: 3000x3000x300: got 18446744072114584320, expected 2700000000
1/1 Test #363: fslio_volsize_64bit ..............***Failed    0.21 sec

With the fix restored it passes.

Worth recording: in a Release build the reverted code passes too. The int product is signed overflow, so it is undefined, and clang at -O2 on arm64 folds it to the correct 64-bit result; at -O0 it yields 0. That is the argument for the fix rather than against it — the current code's correctness depends on the optimization level — but it means the red proof only reproduces in an unoptimized build, and the registered test would not have caught this on a Release-only dashboard.

Build and test

Configured with -DNIFTI_BUILD_APPLICATIONS=ON -DUSE_NIFTI2_CODE=ON -DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON, Release and Debug. 363/363 tests pass in both. No whitespace churn: git diff -w master HEAD and git diff master HEAD show the same 51 added lines.

Follow-up for the maintainer, not fixed here. FslSeekVolume() immediately above FslGetVolSize() still truncates:

int offset;
offset = fslio->niftiptr->iname_offset +
  vols * FslGetVolSize(fslio) * fslio->niftiptr->nbyper;

Now that FslGetVolSize() returns a correct size_t, the product is formed at 64 bits and then assigned to an int, so seeking past 2GB is still wrong — the truncation just moved one line down. Fixing it means changing offset to long long and matching znzseek()'s offset type. I left it out to keep this PR to its stated scope; say the word and it can be folded in.

@hjmjohnson
hjmjohnson force-pushed the fix/fslio-64bit-arithmetic branch from 4e363e6 to d0d5c2d Compare September 22, 2026 15:54
@hjmjohnson
hjmjohnson force-pushed the fix/fslio-64bit-arithmetic branch from d0d5c2d to 3179ff6 Compare September 23, 2026 00:47
A byte count or file offset is declared size_t, but the expression
assigned to it multiplies ints, so the arithmetic happens at 32 bits
and only the result is widened:

    size_t FslGetVolSize(...)
    { return (nx * ny * nz); }           /* int product */

    offset = ((ydim * zVox + yVox) * xdim + xVox) * wordsize;

For a volume over 2GB the product overflows before it is ever assigned
to the size_t, so the destination type buys nothing.  The signed
overflow is undefined, so whether the wide value survives is up to the
optimizer: clang folds it to the correct 64-bit result at -O2 and
yields 0 at -O0.  Affects FslGetVolSize(), FslWriteVolumes(),
FslReadSliceSeries(), FslReadRowSeries() and FslReadTimeSeries().
Each operand is now widened before the multiply rather than after.

The slbytes and volbytes assignments in FslReadSliceSeries() and
FslReadTimeSeries() are left alone because another open pull request
changes those exact lines.
The test drives FslGetVolSize() with dimensions whose product exceeds
INT_MAX and asserts the exact 64-bit value, plus a small volume so an
over-broad change cannot pass it.  It fails in a -O0 build without the
widening fix and passes with it; at -O2 clang folds the signed overflow
to the wide result, so only the unoptimized build observes it.
@hjmjohnson
hjmjohnson force-pushed the fix/fslio-64bit-arithmetic branch from 3179ff6 to 6ba8f4e Compare September 23, 2026 00:56
@hjmjohnson
hjmjohnson merged commit 87b5264 into InsightSoftwareConsortium:master Sep 23, 2026
26 checks passed
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.

3 participants