What happens
nifti_image_from_ascii() converts every numeric attribute with
/* nifti2_io.c:8723 */
#define QQNUM(n1,n2,tt) if( strcmp(lhs,#n1)==0 ) nim->n2=(tt)(strtod(rhs,NULL))
#define QNUM(nam,tt) QQNUM(nam,nam,tt)
strtod() accepts the whole double range, and the result is cast to the
field's type. A value outside the range of the destination integer type is
undefined behaviour (C17 6.3.1.4), not a wrapped or clamped value.
The two header converters do the same thing with vox_offset:
/* nifti2_io.c:4961, 4964; nifti1_io.c:3920, 3923 */
ioff = (int)nhdr.vox_offset ;
Reproducing
bad_cast.nia:
<nifti_image
nifti_type = 'NIFTI-1A'
header_filename = 'bad_cast.nia'
image_filename = 'bad_cast.nia'
ndim = '3'
nx = '1e300'
ny = '4'
nz = '4'
dx = '1'
dy = '1'
dz = '1'
datatype = '16'
intent_code = '1e300'
/>
Built with -fsanitize=undefined:
$ nifti_tool -disp_nim -infiles bad_cast.nia
nifti2_io.c:8849:11: runtime error: 1e+300 is outside the range of
representable values of type 'int'
#0 nifti_image_from_ascii nifti2_io.c:8849
#1 nifti_read_ascii_image nifti2_io.c:6159
#2 nifti_image_read nifti2_io.c:5933
#3 nt_image_read nifti_tool.c:7372
#4 act_disp_nims nifti_tool.c:3258
Line 8849 is QNUM(nx,int). Every integer-typed QNUM behaves the same
way; there are about a dozen of them (image_offset, datatype, ndim,
nx through nw, intent_code, xyz_units, time_units, qform_code,
sform_code, the slice fields), in nifti2_io.c from line 8846 and in
nifti1_io.c from line 6726.
On x86-64 the result is INT_MIN and nothing else goes wrong, so this is a
correctness and portability question rather than a memory-safety one. The
float-typed QNUMs are fine: 1e300 is representable in a double field
and saturates to infinity in a float one, which is defined.
Why an issue and not a pull request
The fix has to choose, and the choice is yours:
- clamp the value into the destination range, which keeps the current
"not a lot of error checking is done here" contract the function
documents, or
- reject the header, which is what the binary paths do for a bad
datatype or dim[].
Either way the integer fields need to be separated from the float ones,
because a clamp that applies to both would break cal_max = '1e30' and
similar legitimate values. That is a change to every QNUM line in two
files, so it seems worth agreeing on before anyone writes it.
Found while fuzzing nifti_image_from_ascii() with clang's libFuzzer under
UndefinedBehaviorSanitizer, alongside the defects in #63 to #69.
What happens
nifti_image_from_ascii()converts every numeric attribute withstrtod()accepts the whole double range, and the result is cast to thefield's type. A value outside the range of the destination integer type is
undefined behaviour (C17 6.3.1.4), not a wrapped or clamped value.
The two header converters do the same thing with
vox_offset:Reproducing
bad_cast.nia:Built with
-fsanitize=undefined:Line 8849 is
QNUM(nx,int). Every integer-typedQNUMbehaves the sameway; there are about a dozen of them (
image_offset,datatype,ndim,nxthroughnw,intent_code,xyz_units,time_units,qform_code,sform_code, the slice fields), innifti2_io.cfrom line 8846 and innifti1_io.cfrom line 6726.On x86-64 the result is
INT_MINand nothing else goes wrong, so this is acorrectness and portability question rather than a memory-safety one. The
float-typed
QNUMs are fine:1e300is representable in adoublefieldand saturates to infinity in a
floatone, which is defined.Why an issue and not a pull request
The fix has to choose, and the choice is yours:
"not a lot of error checking is done here" contract the function
documents, or
datatypeordim[].Either way the integer fields need to be separated from the float ones,
because a clamp that applies to both would break
cal_max = '1e30'andsimilar legitimate values. That is a change to every
QNUMline in twofiles, so it seems worth agreeing on before anyone writes it.
Found while fuzzing
nifti_image_from_ascii()with clang's libFuzzer underUndefinedBehaviorSanitizer, alongside the defects in #63 to #69.