Skip to content

BUG: Check the allocations whose result is used immediately - #55

Merged
hjmjohnson merged 3 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-alloc-null-checks
Sep 22, 2026
Merged

hjmjohnson merged 3 commits into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-alloc-null-checks

Conversation

@gdevenyi

@gdevenyi gdevenyi commented Aug 15, 2026 •

Copy link
Copy Markdown

Nine allocations had their result used without a NULL check, in every
case within a line or two, so an allocation failure is a null
dereference rather than an error.

fslio.c FslInit() calloc(FSLIO), then FslSetInit()
writes through the pointer
FslGetHdrImgNames() two callocs, then strcpy into both
FslWriteVolumes() calloc of the byte-swap buffer,
then written in the reorder loop
FslClose() calloc(dsr), then FslReadRawHeader()
fills it in
nifti2_io.c nifti_read_n2_hdr() malloc(nifti_2_header), then
nifti_convert_nim2n2hdr() fills it
afni_xml.c new_afni_xml(), strdup results stored straight
axml_add_attrs() into the structure

fslio.c already uses FSLIOERR to report allocation failure (d3matrix,
d4matrix), so these follow that idiom rather than adding a second
convention to one file. The others return an error the way the failure
paths beside them do.

One line further on, nifti_read_n2_hdr() released the image it had just
converted with free(nim) rather than nifti_image_free(), leaking the
filename strings nifti_read_ascii_image() had allocated. That is
corrected in the same place.

Deliberately not included, to avoid overlapping open PRs:


Interface impact: none. On the union of all these changes, configured with USE_FSL_CODE=ON and USE_CIFTI_CODE=ON: all 448 exported symbols across libniftiio, libnifti2, libznz, libfslio, libnifticdf and libcifti are identical to master under nm -D --defined-only, and all ten installed headers are identical under gcc -E -P. Under gcc -dM -E one macro definition differs, intentionally and only in text: #61 makes FSL_RADIOLOGICAL read (-1) so it is safe inside an expression. Its value is still -1, checked by compiling against each installed fslio.h and printing it.

Verification. This branch: builds with gcc 16.1.1, ctest unchanged from master (2 of 345 fail on master itself in this environment; #31 and #29 each fix one). The union of all the PRs: 0 errors under both gcc 16.1.1 and clang 22.1.8, ctest 345/345 under each, and the whole suite under valgrind memcheck with --trace-children=yes gives 484 traced processes with no invalid access, no uninitialised value and no leak in any nifti binary.

Coordination. Every line of every branch was compared, whitespace-normalised, against the diffs of the open PRs (#11, #21, #22, #23, #24). Where one of those already changes a line, the line was left alone, and the few deliberate overlaps are named in the text above. What survives is 17 compiler warnings, all of them on those lines: 9 -Wsign-conversion (5 in fslio.c for #22, 2 in nifti2_io.c and 2 in nifti_tester001.c for #24) and 8 -Wcalloc-transposed-args in nifti_findhdrname and nifti_findimgname, which #11 rewrites. No formatting changes appear anywhere, to stay clear of #10 and #12.

One of a set of independent, single-purpose PRs. Each bases on master and can be merged on its own, in any order.

The full set of PRs (35)

The union of all of them is on the fork as all-changes, if you want to build and test the lot at once.

CI and build

Configuration and documentation

Defects

Warning and check classes

This was referenced Aug 15, 2026
@gdevenyi

Copy link
Copy Markdown
Author

Pushed a second commit. The error return this PR added to axml_add_attrs() was being discarded: make_afni_xml() called it and ignored the result, so an element could still be returned with a partial attribute list and nothing would report it.

It now propagates. epush() already treats a NULL from make_afni_xml() by entering a skip block, which is what the allocation-failure paths beside it do.

Worth recording why the attrs.length = aind+1 in the first commit is load-bearing rather than tidy: the two attribute arrays come from malloc(), not calloc(), so entries past the failing pair hold uninitialised pointers. Lowering attrs.length is what stops axml_free_xml_t() reading them, while still freeing the half of the failing pair that was copied.

Note this code is behind USE_CIFTI_CODE, which defaults to OFF, so none of the cifti PRs are compile-tested by the current workflows. Verified locally with -DUSE_CIFTI_CODE=ON.

@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto master b4876bf, commit messages cleaned — and split. The four fsliolib/fslio.c hunks have been removed from this branch and need a maintainer decision before they can land anywhere.

Why the split

All four fslio hunks report the allocation failure through FSLIOERR, and that macro is:

/* fsliolib/fslio.c:46 */
#define FSLIOERR(x) { fprintf(stderr,"Error:: %s\n",(x)); fflush(stderr); exit(EXIT_FAILURE); }

So the change adds four new exit() call sites to a library. This project has a standing rule that a library must not terminate its host process — nifti_clib is vendored into ITK, AFNI, FSL, dcm2niix and Slicer, and a malloc failure inside an image reader should not take the host down with it. PR #74 in this same series is removing an exit(13) from nifti_image_read() for exactly that reason, so landing these four would be pulling in the opposite direction in the same release.

The PR's own reasoning for using FSLIOERR is sound on its own terms — d3matrix() and d4matrix() already use it, and adding a second error convention to one file is its own kind of mess. That is a real trade-off, which is why I have not resolved it unilaterally:

  • FslInit() returns FSLIO*, so it can return NULL. Cheap to fix properly, but it is an API contract change for every caller that does not currently check.
  • FslGetHdrImgNames() returns void. It has no way to report anything without a signature change.
  • FslWriteVolumes() returns size_t and FslClose() returns int. Both could return an error value.

So "just return instead of exiting" is not uniformly available, and where it is, it changes published signatures or contracts. That is the decision I am asking for, and it is bigger than this PR: it is whether FSLIOERR itself should stop calling exit(), which would change the behavior of every existing use in the file, not just these four.

Nothing is lost by holding them — the current code null-dereferences on allocation failure, and so would the FSLIOERR version's caller; neither is a correct library, and the crash is the same class of unrecoverable either way.

What is on the branch now

Two commits, both clean:

  1. BUG: Check the allocations whose result is used immediately — the nifti2_io.c and cifti/afni_xml.c hunks. Each returns an error the way the failure paths beside it already do; no new exit(). Includes the free(nim) to nifti_image_free(nim) correction in nifti_read_n2_hdr().
  2. BUG: Report the attribute failure instead of discarding it — unchanged in substance.
Test: leak fix proven, null checks not demonstrable

The leak is proven. nifti_read_n2_hdr() released a converted image with free(nim), leaking the filename strings nifti_read_ascii_image() had allocated. Reachable through nifti_tool -disp_hdr2 on a NIFTI ASCII (.nia) header.

LeakSanitizer is not available on this platform (AddressSanitizer: detect_leaks is not supported on this platform, macOS arm64), so this is macOS leaks --atExit against a Release build. With the hunk reverted to free(nim):

Process 69003: 2 leaks for 32 total leaked bytes.
4   nifti_tool   0x104b348e0 nifti_read_header + 556
3   nifti_tool   0x104b33b2c nifti_read_n2_hdr + 848
2   nifti_tool   0x104b33d34 nifti_read_ascii_image + 312

With the fix, on the same input: Process 68350: 0 leaks for 0 total leaked bytes.

I did not register this as a ctest. It needs a leak checker to observe, the project has no leak-checking test infrastructure, and a test that runs nifti_tool -disp_hdr2 without one would pass identically with and without the fix — which is worse than no test. If you want leak coverage in CI, the thing to add is a Linux ASan/LSan job, and that is a separate change.

The null checks are NOT-DEMONSTRABLE. Every one of them fires only when malloc/strdup returns NULL. There is no allocation-failure injection harness in the tree, and I did not invent one for three call sites. The argument for them is by inspection: the result is dereferenced within a line or two in each case.

Message cleanup and build

Removed the Co-Authored-By: naming an AI tool and the Claude-Session: URLs from both commits. Replaced the bare #11 and #23 cross-references with words (this is a fork, so those numbers name different PRs upstream). Rewrote the first commit's body to describe three allocations rather than nine, and to record why the fslio four were held. Corrected one British spelling. Trimmed a three-line comment in make_afni_xml() to one line per the project's comment rules.

Build: -DNIFTI_BUILD_APPLICATIONS=ON -DUSE_NIFTI2_CODE=ON -DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON. 362/362 pass. No whitespace churn.

gdevenyi and others added 3 commits September 22, 2026 10:18
Three allocations had their result used without a NULL check, in every
case within a line or two, so an allocation failure is a null
dereference rather than an error.

  nifti2_io.c  nifti_read_n2_hdr()   malloc(nifti_2_header), then
                                     nifti_convert_nim2n2hdr() fills it
  afni_xml.c   new_afni_xml(),       strdup results stored straight
               axml_add_attrs()      into the structure

Each returns an error the way the failure paths beside it do.

One line further on, nifti_read_n2_hdr() released the image it had
just converted with free(nim) rather than nifti_image_free(), leaking
the filename strings nifti_read_ascii_image() had allocated.  That is
corrected in the same place.

The four fslio.c allocations in the same class are left out: the
idiom that file uses to report an allocation failure, FSLIOERR, calls
exit(), and whether the library may end its host process is for the
maintainers to decide.
axml_add_attrs() returns 1 when it cannot copy an attribute, but
make_afni_xml() ignored the result, so the element was returned with a
partial attribute list and nothing said so.

Every failure the function reports is an allocation failure, so there
is no case where continuing is right.  Propagate it: epush() already
treats a NULL from make_afni_xml() by entering a skip block, which is
what the allocation-failure paths beside it do.

axml_free_xml_t() releases what was built.  It walks the attributes
with attrs.length, which axml_add_attrs() lowers to the pair it failed
on, so the half of that pair that was copied is freed and the entries
past it are never read.  That matters because the two arrays come from
malloc(), not calloc(), so those entries hold uninitialized pointers.
nifti_read_n2_hdr() builds a nifti_image to convert an ASCII header and
then released it with free(), which leaves its filename strings behind.
No test reached that path, so the suite reported 362 of 362 either way.

The fixture is the one the ASCII attribute test already uses, read
through -disp_hdr2 rather than -disp_nim so the NIFTI-2 reader is the
one exercised. The leak is visible to the memcheck and sanitizer legs;
an ordinary build stays green with or without the fix.
@hjmjohnson
hjmjohnson force-pushed the pr/fix-alloc-null-checks branch from f8b9662 to a1e0d8c Compare September 22, 2026 15:21
@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto current master and split: the four fsliolib hunks are gone, so this no longer adds any exit() to library code and the policy objection against it is resolved. What remains is three allocation checks and one leak fix, plus a regression test for the leak.

Why the fsliolib hunks were removed rather than rewritten

They reported failure through FSLIOERR, which is

#define FSLIOERR(x) { ... ; exit(EXIT_FAILURE); }

and this project's standing position is that a library returns errors rather than terminating its host — it is vendored into ITK, AFNI, FSL, dcm2niix and Slicer, where an exit() takes down a pipeline with no chance to recover or clean up.

Rewriting them in place is not uniformly possible: FslGetHdrImgNames() returns void, and having FslInit() return NULL is an API contract change. The real question is whether FSLIOERR itself should stop calling exit(), which is larger than this PR and belongs in its own change.

Worth knowing before anyone takes that on: roughly 50 call sites are written as braceless if (...) FSLIOERR(...);, and they are correct only because the macro never returns. Making it non-fatal breaks all of them silently. Two other open PRs touch the same knot.

The leak, and a test that catches it

nifti_read_n2_hdr() builds a nifti_image to convert an ASCII header, then released it with free(nim) — which leaves nim's filename strings behind. The fix is nifti_image_free(nim).

Nothing reached that path, so I measured rather than assumed: with the fix reverted, the suite still reported 362 of 362 passing. The path is reachable from a shipped tool, though, and the leak is observable:

nifti_tool -disp_hdr2 -infiles dup_attr.nia
  -> ERROR: LeakSanitizer: detected memory leaks
     SUMMARY: AddressSanitizer: 18 byte(s) leaked in 2 allocation(s)

So the test is registered. It uses the fixture the ASCII attribute test already carries, read through -disp_hdr2 rather than -disp_nim so the NIFTI-2 reader is the one exercised.

Red proof, reverting only the free(nim) change and keeping the test:

GREEN (with the fix)      100% tests passed, 0 failed out of 363
RED   (fix reverted)       99% tests passed, 1 failed out of 363
        360 - nifti_tool_disp_hdr2_ascii (Failed)
        SUMMARY: AddressSanitizer: 18 byte(s) leaked in 2 allocation(s)

The test only bites under a leak checker. A plain Release build is 363/363 with or without the fix, confirmed. It fails on the sanitize-clang-linux and valgrind-gcc-linux legs, which is where it is meant to.

The allocation checks

Three, all in cifti/afni_xml.c, none of them reachable without an allocation failure and so none separately testable:

  • new_afni_xml() checks the strdup of the element name and frees the node on failure.
  • axml_add_attrs() checks both strdups and sets attrs.length so the partially-filled pair is still released by the caller's free.
  • make_afni_xml() stops discarding axml_add_attrs()'s return and frees the node when it fails.

@hjmjohnson
hjmjohnson merged commit c6410f5 into InsightSoftwareConsortium:master Sep 22, 2026
25 checks passed
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