Skip to content

ENH: Make the 64-to-32 bit truncations explicit - #52

Merged
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-shorten-64-to-32
Sep 23, 2026
Merged

hjmjohnson merged 1 commit into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-shorten-64-to-32

Conversation

@gdevenyi

@gdevenyi gdevenyi commented Aug 15, 2026 •

Copy link
Copy Markdown

31 -Wshorten-64-to-32 warnings. Most were benign; one was not.

FslSeekVolume() declared int offset and assigned it a byte position
into the image file:

offset = fslio->niftiptr->iname_offset
         + vols * FslGetVolSize(fslio) * fslio->niftiptr->nbyper;
return znzseek(fslio->fileptr, offset, SEEK_SET);

Seek to any volume past 2GB and the multiplication overflows, so znzseek
-- which takes a 64-bit znz_off_t -- receives a wrong and quite possibly
negative position. offset is a local, so widening it to znz_off_t and
widening the operands changes no interface. FslReadVolumes' volbytes had
the same shape and is now size_t.

The rest fall into two groups. Internal helpers were widened to carry
the value they were already being handed: nifti_read_extensions() and
rci_alloc_mem() now return int64_t rather than truncating their own
results, and nifti_read_next_extension() and nifti_check_extension() take
an int64_t remain. nt_read_bricks(), declared in the uninstalled
nifti_tool.h, takes an int64_t length.

Where the narrowing target is an installed prototype it can only be made
explicit, so each such cast carries a note: nifti_image_load_bricks()
returning a brick count as int, nifti_read_subregion_image() returning a
byte count as int, nifti_read_ascii_image() taking an int header length,
and FslSeekVolume()'s int return. The remainder are strlen() results
assigned to int and fread()/znzread() counts, bounded by buffers a few
hundred bytes long.

Two format strings were widened to PRId64 to match the parameters that
changed.

A second commit extends the same fix to the rest of fslio's size
arithmetic. FslGetVolSize(), FslWriteVolumes(), FslReadSliceSeries(),
FslReadRowSeries() and FslReadTimeSeries() each declare a size_t byte
count or file offset but compute it as a product of ints, so the
arithmetic overflows at 32 bits before it is ever widened. Each operand
is now widened before the multiply. Two instances -- the slbytes and
volbytes assignments in FslReadSliceSeries() and FslReadTimeSeries() --
are deliberately left alone because upstream PR #22 changes those exact
lines.

Interface impact: none. On the union of all these changes, configured with USE_FSL_CODE=ON and USE_CIFTI_CODE=ON: all 448 exported symbols across libniftiio, libnifti2, libznz, libfslio, libnifticdf and libcifti are identical to master under nm -D --defined-only, and all ten installed headers are identical under gcc -E -P. Under gcc -dM -E one macro definition differs, intentionally and only in text: #61 makes FSL_RADIOLOGICAL read (-1) so it is safe inside an expression. Its value is still -1, checked by compiling against each installed fslio.h and printing it.

Verification. This branch: builds with gcc 16.1.1, ctest unchanged from master (2 of 345 fail on master itself in this environment; #31 and #29 each fix one). The union of all the PRs: 0 errors under both gcc 16.1.1 and clang 22.1.8, ctest 345/345 under each, and the whole suite under valgrind memcheck with --trace-children=yes gives 484 traced processes with no invalid access, no uninitialised value and no leak in any nifti binary.

Coordination. Every line of every branch was compared, whitespace-normalised, against the diffs of the open PRs (#11, #21, #22, #23, #24). Where one of those already changes a line, the line was left alone, and the few deliberate overlaps are named in the text above. What survives is 17 compiler warnings, all of them on those lines: 9 -Wsign-conversion (5 in fslio.c for #22, 2 in nifti2_io.c and 2 in nifti_tester001.c for #24) and 8 -Wcalloc-transposed-args in nifti_findhdrname and nifti_findimgname, which #11 rewrites. No formatting changes appear anywhere, to stay clear of #10 and #12.

One of a set of independent, single-purpose PRs. Each bases on master and can be merged on its own, in any order.

The full set of PRs (35)

The union of all of them is on the fork as all-changes, if you want to build and test the lot at once.

CI and build

Configuration and documentation

Defects

Warning and check classes

This was referenced Aug 15, 2026
@gdevenyi
gdevenyi force-pushed the pr/fix-shorten-64-to-32 branch from 842c230 to f53855b Compare August 15, 2026 05:22
@gdevenyi
gdevenyi marked this pull request as ready for review August 15, 2026 05:26
@gdevenyi
gdevenyi force-pushed the pr/fix-shorten-64-to-32 branch from f53855b to 1a235ad Compare September 19, 2026 02:13
@gdevenyi gdevenyi changed the title BUG: Fix 64-to-32 bit truncations, including a real overflow in fslio ENH: Make the 64-to-32 bit truncations explicit Sep 19, 2026
@hjmjohnson
hjmjohnson force-pushed the pr/fix-shorten-64-to-32 branch 2 times, most recently from 164cef1 to 1903564 Compare September 22, 2026 02:10
@hjmjohnson
hjmjohnson force-pushed the pr/fix-shorten-64-to-32 branch from 1903564 to 2b549e4 Compare September 22, 2026 10:48
@hjmjohnson
hjmjohnson force-pushed the pr/fix-shorten-64-to-32 branch from 2b549e4 to 1c81802 Compare September 22, 2026 13:52
@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto current master (b4876bf) and pushed as 1c81802. Applied cleanly, no conflicts. I audited every cast for value preservation and found one that was not; it is fixed in the rebased commit.

One cast was narrowing further than the original code, and is now corrected. In FslSetDimensionality():

fslio->niftiptr->ndim    = (int)dim;
fslio->niftiptr->dim[0]  = (short)dim;   /* was */

nifti_image::dim is int dim[8] (niftilib/nifti1_io.h:122), so (short) narrowed to 16 bits where the original implicit conversion went to 32, and disagreed with the (int) on the line immediately above it. Unreachable in practice — dim[0] is a dimensionality, 1..7 — but it is not the value-preserving change this PR is supposed to be making. Now (int)dim. Every other cast in the diff is value-preserving at the widths involved.

The FslSeekVolume overflow, and why there is no ctest for it

The overflow is real. Reproducing the two expressions standalone, for a 512^3 4-byte volume at volume index 5:

old (int)   : -1610612384
new (off_t) : 2684354912

The truncation is at the assignment to int offset, and znzseek() then receives a negative position, so the seek fails outright rather than landing in the wrong place.

I could not turn this into a ctest, and did not invent one:

  • fsliolib/ has no test target and no test sources at all. Adding one would be a larger change than this PR.
  • The observable needs a volume size past 2GB, which means a header declaring dimensions that large.
  • FslSeekVolume() is declared to return int, so even with the fix the returned offset is truncated. The function's own return value cannot distinguish fixed from broken for the offsets where the bug bites; only the subsequent read position can, and reaching that requires the test target that does not exist.

NOT-DEMONSTRABLE as a ctest. The standalone arithmetic above is the evidence, and it is arithmetic, not a library test — worth saying plainly.

Casts that remain lossy by necessity, for the record

Where the narrowing target is an installed prototype the cast can only be made explicit, not removed. These stay lossy and the PR is right to say so:

  • nifti_read_ascii_image(fp, hfile, (int)filesize, ...) — an ASCII header file past 2GB.
  • disp_raw_data(..., (int)(len64 / nim->nbyper), ...).
  • (int)NBL->nbricks, (int)bytes from nifti_read_subregion_image(), and FslSeekVolume()'s int return.

Each is reachable only through an API whose own parameter is already int, so the caller cannot request a value that would truncate.

What else changed in this update
  • Commit message: dropped the Co-Authored-By: naming an AI tool and the Claude-Session: URL.
  • US English: a behaviour in the new FslSeekVolume() comment. That comment also narrated that behavior was unchanged from before, which the diff already says; cut to one line stating the constraint.
  • Whitespace-churn check: 51 added lines with and without -w. No churn.
Build and test

Release, NIFTI_BUILD_APPLICATIONS=ON USE_NIFTI2_CODE=ON USE_CIFTI_CODE=ON USE_FSL_CODE=ON, Ninja, macOS/AppleClang: build clean apart from the pre-existing duplicate-library link warning, 100% tests passed, 0 tests failed out of 362.

31 -Wshorten-64-to-32 warnings.  Most were benign; one was not.

FslSeekVolume() declared `int offset` and assigned it a byte position
into the image file:

    offset = fslio->niftiptr->iname_offset
             + vols * FslGetVolSize(fslio) * fslio->niftiptr->nbyper;
    return znzseek(fslio->fileptr, offset, SEEK_SET);

Seek to any volume past 2GB and the multiplication overflows, so znzseek
-- which takes a 64-bit znz_off_t -- receives a wrong and quite possibly
negative position.  offset is a local, so widening it to znz_off_t and
widening the operands changes no interface.  FslReadVolumes' volbytes had
the same shape and is now size_t.

The rest fall into two groups.  Internal helpers were widened to carry
the value they were already being handed: nifti_read_extensions() and
rci_alloc_mem() now return int64_t rather than truncating their own
results, and nifti_read_next_extension() and nifti_check_extension() take
an int64_t `remain`.  nt_read_bricks(), declared in the uninstalled
nifti_tool.h, takes an int64_t length.

Where the narrowing target is an installed prototype it can only be made
explicit, so each such cast carries a note: nifti_image_load_bricks()
returning a brick count as int, nifti_read_subregion_image() returning a
byte count as int, nifti_read_ascii_image() taking an int header length,
and FslSeekVolume()'s int return.  The remainder are strlen() results
assigned to int and fread()/znzread() counts, bounded by buffers a few
hundred bytes long.

Two format strings were widened to PRId64 to match the parameters that
changed.
@hjmjohnson
hjmjohnson force-pushed the pr/fix-shorten-64-to-32 branch from 7d3ca4d to f45671d Compare September 23, 2026 00:47
@hjmjohnson
hjmjohnson merged commit 08c5f80 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.

2 participants