Skip to content

BUG: Refuse a header whose dimensions overflow the voxel count - #68

Merged
hjmjohnson merged 2 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:fix/nvox-overflow
Sep 22, 2026
Merged

hjmjohnson merged 2 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:fix/nvox-overflow

Conversation

@gdevenyi

Copy link
Copy Markdown

Both header converters multiply the dimensions into nim->nvox without
checking the product:

for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ )
   nim->nvox *= nhdr.dim[ii];

Seven NIFTI-1 dimensions of 32767 are enough to overflow int64_t, and a
NIFTI-2 header needs only two dimensions to do it:

nifti2_io.c:4838: runtime error: signed integer overflow:
    1152780773560811521 * 32767 cannot be represented in type 'int64_t'

Signed overflow is undefined, and what the compiler does produce is a
voxel count that no longer describes the file. nvox then goes on to
nifti_get_volsize(), which multiplies it by nbyper for another
unchecked product, and that result is used as an allocation size and a
read length.

Both products are now checked before they are made, in the way the
surrounding code already reports a bad header. A header that overflows
is rejected with a message instead of producing a silently wrong image.
No valid image is affected: an int64_t voxel count is more than any
file can hold.

Found by fuzzing the header converters with clang's libFuzzer under
UndefinedBehaviorSanitizer.


How to reproduce, and the sanitizer builds used, are in the commit message. Found with clang libFuzzer harnesses over nifti_image_read(), the two header converters, nifti_image_from_ascii() and axml_read_buf(), run under AddressSanitizer, UndefinedBehaviorSanitizer and MemorySanitizer.

One commit on master, independent of my other open PRs. ctest passes on master with this branch alone (343/343, excluding the nifti_c22_copy_image failure that #31 fixes), and on the union of all my branches under ASan+UBSan, TSan and MSan.

@gdevenyi
gdevenyi marked this pull request as ready for review August 15, 2026 21:30
@gdevenyi

Copy link
Copy Markdown
Author

Pushed a second commit. The first one guards both converters in nifti2/nifti2_io.c, but niftilib/nifti1_io.c has the same loop in nifti_convert_nhdr2nim(), on the same untrusted path, and was left unguarded:

$ nifti1_tool -disp_nim -infiles huge.nii     # dim[0]=7, dim[1..7]=32767
  dim     32  8  7 32767 32767 32767 32767 32767 32767 32767
  nvox    64  1  -1073512449

The wrapped count then becomes the data allocation size and the result of nifti_get_volsize(). With the guard the header is refused instead.

nifti_image.nvox is a size_t in this library and an int64_t in the NIFTI-2 one, so the bound here is SIZE_MAX, not INT64_MAX; nifti_get_volsize() is size_t * size_t to match.

Two more nvox loops remain in each library — nifti_update_dims_from_array() and update_nifti_image_for_brick_list(). Both take dims a caller has already set rather than dims read from a file, and are reached from nifti_tool's command line, so I left them for a separate change rather than widen this one.

gdevenyi and others added 2 commits September 21, 2026 20:02
Both header converters multiply the dimensions into nim->nvox without
checking the product:

    for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ )
       nim->nvox *= nhdr.dim[ii];

Seven NIFTI-1 dimensions of 32767 are enough to overflow int64_t, and a
NIFTI-2 header needs only two dimensions to do it:

    nifti2_io.c:4838: runtime error: signed integer overflow:
        1152780773560811521 * 32767 cannot be represented in type 'int64_t'

Signed overflow is undefined, and what the compiler does produce is a
voxel count that no longer describes the file.  nvox then goes on to
nifti_get_volsize(), which multiplies it by nbyper for another
unchecked product, and that result is used as an allocation size and a
read length.

Both products are now checked before they are made, in the way the
surrounding code already reports a bad header.  A header that overflows
is rejected with a message instead of producing a silently wrong image.
No valid image is affected: an int64_t voxel count is more than any
file can hold.

Found by fuzzing the header converters with clang's libFuzzer under
UndefinedBehaviorSanitizer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014m231RPDZPDbYDVxawxpjG
The first commit guards both converters in nifti2/nifti2_io.c.
niftilib/nifti1_io.c has the same loop in nifti_convert_nhdr2nim(), on
the same untrusted path, and was left unguarded.

  $ nifti1_tool -disp_nim -infiles huge.nii   # dim[0]=7, dim[1..7]=32767
    dim     32  8  7 32767 32767 32767 32767 32767 32767 32767
    nvox    64  1  -1073512449

The wrapped count then becomes the data allocation size and the result
of nifti_get_volsize().  With the guard the header is refused:

  ** ERROR: nifti_convert_nhdr2nim: dim[] overflows the voxel count

nifti_image.nvox is a size_t in this library and an int64_t in the
NIFTI-2 one, so the bound here is SIZE_MAX rather than INT64_MAX.
nifti_get_volsize() is size_t * size_t to match.  dim[0] is already
bounded by need_nhdr_swap(), and the loop that raises every dim to at
least 1 still runs first, so the dim[ii] > 0 test never has to reject.

Two more nvox loops remain in each library, in
nifti_update_dims_from_array() and update_nifti_image_for_brick_list().
Both take dims that a caller has already set rather than dims read from
a file, and reach them from nifti_tool's command line, so they are left
for a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176ZLorDY784us6dFAbE1ZJ
@hjmjohnson
hjmjohnson merged commit 7cd29b1 into InsightSoftwareConsortium:master Sep 22, 2026
21 checks passed
@hjmjohnson

Copy link
Copy Markdown
Member

The commit messages in this range were rewritten to remove trailers that do not belong in permanent history: Co-Authored-By: naming an AI tool, and Claude-Session: URLs that resolve for nobody. Only messages changed — the tree at the tip of master is byte-identical, and author, committer, and dates are preserved.

This PR's commits on the rewritten master:

  • 66c76c22ea BUG: Refuse a header whose dimensions overflow the voxel count
  • 147a07a48f BUG: Guard the voxel count in the NIFTI-1 library too

The SHA recorded above by GitHub is from the pre-rewrite history and no longer resolves.

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