Skip to content

Out-of-range float to integer casts in the ASCII header parser and vox_offset #70

Description

@gdevenyi

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions