Conversation
cefe963 to
a22bf88
Compare
|
@seanm I updated and fixed merge conflicts for this PR. Would you please review and update so that we could incorporate the removal of the "exit()" commands that could cause an application to crash from using this library? |
|
lgtm. What do you want me to update? Not sure I understand that part of your comment... |
|
@seanm At a minimum, the commit message needs to be changed from WIP. |
|
So you've already mutated my commits here, right? That means what's on my local disk is not the same. If I alter my commit message and push, I'll overwrite what you did I think. I'm not sure how to overcome that because my git-fu is weak... |
a22bf88 to
fd54dd7
Compare
|
The This PR changes the two It also fixes the other defects in the same blocks, which are easy to miss because they sit on the lines either side of the free(basename);
char * gzname = (char *)calloc(strlen(hdrname) + 8, sizeof(char));
strcpy(gzname, hdrname); /* gzname unchecked */
...
fprintf(stderr, "... basename (*.nii, *.nii.gz): %s\n",
basename); /* freed above */
exit(134);
Behaviour change worth stating either way: callers that relied on the process dying now get NULL back, which is the point, but a caller that never checked will continue with a NULL pointer where it previously exited. The @seanm — on your last comment about not knowing how to update the commit message after @hjmjohnson rebased your branch: |
Two of the five Build and Test jobs have never run to completion. sanitize-clang-linux invokes scan-build, which lives in clang-tools and was not installed, so the job exits 127 before configuring. rel-clang-macos asks brew for "sed", which is not a formula; brew fails the step and the job exits 1 before configuring. The GNU sed the dashboard scripts expect is gnu-sed. Both predate the workflow's first successful run, so neither has regressed; the trigger named the wrong branch until recently and the jobs never executed.
…onsortium/fix/buildyml-jobs COMP: Install the tools the analysis jobs actually invoke
loc_strnlen measures a string that need not be terminated, but it dereferences before testing the bound, so when no NUL appears in the first maxlen bytes the last iteration reads str[maxlen]. Both callers pass an unterminated buffer: axml_read_buf takes the caller's buffer and its length, and axml_read_file measures what fread returned, which fills the buffer for any larger file. AddressSanitizer on a buffer sized to its content reports a heap-buffer-overflow read 0 bytes after a 324-byte region. The returned length is unchanged wherever the old order was in bounds.
…l-strnlen-bound BUG: Stop loc_strnlen reading one byte past the buffer it is given
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
…rter
nifti_convert_n2hdr2nim() uses nhdr.dim[0], the number of dimensions,
as a loop bound over the eight-element dim[] array:
for( ii=2 ; ii <= nhdr.dim[0] ; ii++ ) ...
for( ii=nhdr.dim[0]+1 ; ii <= 7 ; ii++ ) ...
for( ii=1 ; ii <= nhdr.dim[0] ; ii++ ) ...
It never checks that dim[0] is in [0,7]. The NIFTI-1 converter gets
that check for free, because need_nhdr_swap() rejects a dim[0] outside
[1,7] in either byte order, but the NIFTI-2 path decides swapping from
sizeof_hdr and reaches the loops with whatever the file said.
A NIFTI-2 header with a large negative dim[0] therefore starts the
second loop at a wild negative index, reads far outside the header and
segfaults. This is not confined to the header API: nifti_image_read()
gets there for any file with a valid 540 byte NIFTI-2 header, so a
604 byte file crashes nifti_tool:
$ nifti_tool -disp_nim -infiles bad_n2_dim0.nii
nifti2_io.c:5079: runtime error: index -6727636073941130588 out of
bounds for type 'int64_t[8]'
AddressSanitizer: SEGV ... in nifti_convert_n2hdr2nim
dim[0] is now range checked in the same place, and in the same style,
as the dim[1] check just below it. Zero stays acceptable, as it is on
the NIFTI-1 side. Valid headers are unaffected: dim[0] outside [0,7]
has no meaning in either format.
Found by fuzzing nifti_convert_n2hdr2nim() with clang's libFuzzer under
AddressSanitizer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014m231RPDZPDbYDVxawxpjG
…agic claims
swap_nifti_header() takes a void pointer and a version number, and picks
the struct to swap from the version:
if ( ni_ver == 0 ) nifti_swap_as_analyze((nifti_analyze75 *)hdr);
else if( ni_ver == 1 ) nifti_swap_as_nifti1((nifti_1_header *)hdr);
else if( ni_ver == 2 ) nifti_swap_as_nifti2((nifti_2_header *)hdr);
Two callers pass it a nifti_1_header, 348 bytes, together with the
version taken from that header's own magic string by NIFTI_VERSION().
A file whose magic is "n+2" therefore has 540 bytes swapped in place:
192 bytes of read and write past the end of the caller's stack object.
nifti_read_n1_hdr() reads the header straight from a file, so the bytes
that decide this come from the file being read. A 348 byte file with
sizeof_hdr = 348, dim[0] byte-swapped, magic = "n+2"
is enough; need_nhdr_swap() then reports that swapping is needed and the
overflow happens before any validity check. AddressSanitizer:
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 1 ...
#0 nifti_swap_4bytes nifti2_io.c:3051
InsightSoftwareConsortium#1 nifti_swap_as_nifti2 nifti2_io.c:3194
InsightSoftwareConsortium#2 nifti_read_n1_hdr nifti2_io.c:5394
Address ... is located in stack of thread T0 at offset 412 in frame
[64, 412) 'nhdr' (line 5339) <== Memory access ... overflows
nifti_convert_n1hdr2nim() has the same line and takes its header from
the caller, so any application that fills a nifti_1_header itself can
reach it too.
Both call sites know the struct they hold, so both now ask for the swap
that struct supports: analyze when the magic is absent, NIFTI-1
otherwise. Headers claiming versions 3 to 9, which swap_nifti_header()
previously refused with a message and left unswapped, are now swapped as
NIFTI-1 as well, which is the only interpretation 348 bytes allow.
nifti1_io.c is not affected: its swap_nifti_header() takes a
nifti_1_header * and a flag, so it cannot choose a wider struct.
Found by fuzzing nifti_convert_n1hdr2nim() with clang's libFuzzer under
AddressSanitizer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014m231RPDZPDbYDVxawxpjG
nifti_tool.c has the same defect this branch fixes in the library: it
passes NIFTI_VERSION(*nhdr), read from the magic string, to
swap_nifti_header(), which selects the struct to swap by that number.
act_mod_hdrs() and act_swap_hdrs() hold a nifti_1_header of 348 bytes.
A magic of "n+2" makes NIFTI_VERSION() return 2, so swap_nifti_header()
treats the allocation as a 540 byte nifti_2_header and reads and writes
past its end. Both functions refuse a header that is valid NIFTI-2, but
a header that is valid as neither version reaches the call.
$ printf ... > evil.nii # 348 bytes, sizeof_hdr byte swapped,
# magic "n+2"
$ nifti_tool -mod_hdr -mod_field descrip hello -overwrite \
-infiles evil.nii
==2747364==ERROR: AddressSanitizer: heap-buffer-overflow
#0 nifti_swap_4bytes nifti2_io.c:3029
InsightSoftwareConsortium#1 nifti_swap_as_nifti2 nifti2_io.c:3172
InsightSoftwareConsortium#2 swap_nifti_header nifti2_io.c:3131
InsightSoftwareConsortium#3 act_mod_hdrs nifti_tool.c:3389
0 bytes after 348-byte region allocated in nifti_read_n1_hdr()
Both now pass 1, or 0 for ANALYZE, which are the two 348 byte layouts.
act_mod_hdr2s() holds a nifti_2_header and gets the explicit 2, as
act_swap_hdrs() already does for its own NIFTI-2 display path.
old_swap_nifti_header() takes a nifti_1_header and a boolean, so the
calls beside these need no change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176ZLorDY784us6dFAbE1ZJ
The only file in the tree without a trailing newline, and the only -Wnewline-eof warning. C11 5.1.1.2p1 requires a non-empty source file to end in a newline not immediately preceded by a backslash, so this is undefined behaviour rather than only a diff annoyance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSPnbwpDjVcAYqDdVqLkMU
fd54dd7 to
d786e5f
Compare
…newline-eof STYLE: Add the missing newline at end of nifti1_tool.h
…dim0-range BUG: Check dim[0] before using it to index dim[] in the NIFTI-2 converter
…x-overflow BUG: Refuse a header whose dimensions overflow the voxel count
…1-swap-width BUG: Swap a nifti_1_header as a NIFTI-1 header, not as whatever its magic claims
In C++ library code, it’s generally considered bad practice to use assert for input validation or error handling. --Terminates the program exit causes dependant applications to terminate - not ideal for libraries meant to be reused safely by others. --Not user-friendly Gives no chance to recover or handle the failure gracefully. --Violates encapsulation Forces application behavior based on internal library assumptions.
d786e5f to
e584a0e
Compare
|
Superseded by #54, which merged as part of the same cleanup and makes the identical change in both copies of Rebasing this branch onto current Why #54 was taken insteadBoth replace free(gzname); free(basename); free(hdrname);
return NULL;where this branch frees only The |
No description provided.