BUG: Refuse a header whose dimensions overflow the voxel count - #68
Conversation
|
Pushed a second commit. The first one guards both converters in The wrapped count then becomes the data allocation size and the result of
Two more |
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
03942e2 to
191f84f
Compare
7cd29b1
into
InsightSoftwareConsortium:master
|
The commit messages in this range were rewritten to remove trailers that do not belong in permanent history: This PR's commits on the rewritten
The SHA recorded above by GitHub is from the pre-rewrite history and no longer resolves. |
Both header converters multiply the dimensions into nim->nvox without
checking the product:
Seven NIFTI-1 dimensions of 32767 are enough to overflow int64_t, and a
NIFTI-2 header needs only two dimensions to do it:
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()andaxml_read_buf(), run under AddressSanitizer, UndefinedBehaviorSanitizer and MemorySanitizer.One commit on master, independent of my other open PRs.
ctestpasses on master with this branch alone (343/343, excluding thenifti_c22_copy_imagefailure that #31 fixes), and on the union of all my branches under ASan+UBSan, TSan and MSan.