Skip to content

ENH: Add regression tests for the bug fixes of the past two days - #80

Merged
hjmjohnson merged 11 commits into
masterfrom
test/regression-coverage
Sep 22, 2026
Merged

hjmjohnson merged 11 commits into
masterfrom
test/regression-coverage

Conversation

@hjmjohnson

Copy link
Copy Markdown
Member

Adds a regression test for each of the ten bug fixes that landed on master over the past two days, plus two fixes for bugs the tests themselves uncovered. Every test was verified by reverting its fix and watching it go red; none of these fixes has any coverage today, and the suite stays green with any of them reverted.

What each test covers, and the observed red
Fix reverted Test that goes red How it fails Config
7de7c95 dim[0] range nifti_tool_disp_nim_bad_dim0 SEGFAULT Release
352297b library swap width check_hdr_magic_n2 + the 2 tool tests Subprocess aborted ASan
fb83d99 tool swap width mod_hdr_magic_n2, swap_as_old_magic_n2 Subprocess aborted ASan
9ab39c6 nvox overflow the 3 nifti_tool refusal tests wrong output Release
10068b9 nvox overflow, NIFTI-1 the 2 nifti1_tool refusal tests wrong output Release
8de2af0 ASCII repeated attribute both ascii_dup_attr tests leak report ASan+LSan
744e751 header failure paths mod_hdr_errpaths leak report ASan+LSan
0cf25aa loc_strnlen bound cifti_tool_unterminated_cext Subprocess aborted ASan
fb6035b ambiguous filename both ambiguous_hdrname tests exit 134, not 1 FSLSTYLE=ON
510abc1 content comparison nii_cmp_selftest identical content reported different Release

Reverting 352297b also reddens the two fb83d99 tests, because those tool paths read their header through the library. That is expected rather than an over-broad test.

Two bugs the tests found

The REJECT_COMPLEX block reads nim->datatype before the NULL check below it. Six of these tests segfaulted under FSLSTYLE=ON with every fix in place:

nim = nifti_convert_nhdr2nim(nhdr,hfile);   /* returns NULL on refusal */
#ifdef REJECT_COMPLEX
if ((nim->datatype == DT_COMPLEX64) || ...   /* dereferenced here */
#endif
if( nim == NULL ){                           /* checked here */

Present in both libraries. The guards added over the past two days make the converter return NULL for exactly the malformed headers these fixtures carry, so every REJECT_COMPLEX build crashed on a header it had just correctly refused. Only the order was wrong; the refusal itself was already right.

cifti_tool never frees the tree it parses. process() returned without releasing the afni_xml_t or the nifti_image. These are the first tests to run cifti_tool under a sanitizer, and LeakSanitizer is on by default under AddressSanitizer on Linux but not on Apple, so it fails there only. The same fix is in #40 — drop that commit from this branch if #40 merges first.

Fixtures and seams

Nine fixtures, 352 to 704 bytes, all in tree; none needs the external testing data. Each per-commit report records the Python that generates it, so the bytes are reproducible rather than opaque blobs. Two are shared: one header drives three tests across 352297b and fb83d99, and one drives both halves of the 9ab39c6/10068b9 pair.

Seam precision worth knowing:

  • -check_hdr reaches the library swap; -disp_hdr does not.
  • For loc_strnlen, -as_cext is the wrong seam — axml_read_file allocates one spare byte and absorbs the overread. Only axml_read_buf, where the buffer is an extension's edata allocated esize-8 and filled exactly, reaches it.
  • The ambiguous-filename tests need a cmake -P driver, because the expected status is exit 1 and plain add_test cannot tell that from the abort it replaced. They are guarded on FSLSTYLE_NAME_CONFLICTS, so they register only where the code is compiled.
  • mod_hdr_errpaths cannot assert merely "the command failed": both invocations are expected to fail, and a sanitizer abort exits nonzero too. It asserts the expected diagnostic and the absence of a sanitizer report.

Where a fix makes something stop happening, a companion assertion pins what must still happen — the recognized element still displays, the last repeated attribute still wins, an unambiguous name still resolves, differing content is still reported as different. Without those, an over-broad fix that refused everything would pass.

Verification
Configuration Result
Apple, Release, cifti + FSL + apps 360/360
Apple, Debug + ASan 360/360
Apple, Release, FSLSTYLE=ON 362/362
Linux, Debug + ASan + LSan 360/360

Every revert above was run in an isolated worktree with its own build tree, and the tree restored to 360/360 after each.

Two tests are only red under a sanitizer (0cf25aa, and the two leak fixes), and two only under FSLSTYLE=ON. Those legs exist in CI, so the tests do run — but they are not guarded by the ordinary Release matrix, and that is stated rather than implied.

…atype

The REJECT_COMPLEX block reads nim->datatype, and the test for nim being
NULL sits immediately below it, so a header the converter refuses crashes
the process in builds that define REJECT_COMPLEX. The refusal itself is
correct and already reported; only the order is wrong.

Found by the malformed-header tests added in this branch: the guards make
the converter return NULL for exactly the inputs they cover, which is the
case this block never handled.
A dim[0] outside 1..7 is used to index dim[] in nifti_convert_n2hdr2nim,
so a header carrying a large negative value reads far outside the struct.
The fixture is a 604-byte NIFTI-2 header holding such a value; without the
check the tool segfaults in an ordinary Release build, so this test guards
on every CI job rather than only the sanitizer one.
nifti_read_n1_hdr reads 348 bytes, so swapping the buffer as whatever
width its magic names reaches past the end when the magic says n+2. The
fixture is a big-endian NIFTI-1 header wearing an n+2 magic, which the
sanitizer legs report as a stack-buffer-overflow without the fix.

-check_hdr is the seam that reaches it; -disp_hdr does not.
act_mod_hdrs() and act_swap_hdrs() hold a 348-byte nifti_1_header and
swapped it at the width its magic named, reading past the end for an n+2
magic. Both reuse the fixture added for the library-side check, and both
rewrite their input, so each test runs against its own copy.
Three fixtures, because the two guards are reached separately: a product
that wraps nvox, the NIFTI-2 form of the same, and one whose nvox fits
while nvox * nbyper does not. Without the guards the tool accepts all
three and reports a negative nvox.

The regex pins which refusal fired; an exit-code test would not tell
this rejection apart from any other error.
nifti_convert_nhdr2nim carries its own copy of the guards, so it needs
its own tests; reverting the NIFTI-1 half alone leaves the nifti2 tests
green and turns exactly these two red.
nifti_image_from_ascii overwrote fname and iname without releasing what
they already held, so an ASCII header naming either attribute twice leaked
the earlier copy. One fixture drives both libraries.

The regex asserts the last value survives, so a fix that ignored the
repeat instead of freeing the previous value could not pass.
loc_strnlen read one byte past the buffer it was given. The reachable
caller is axml_read_buf, where the buffer is an extension's edata,
allocated esize-8 and filled exactly; axml_read_file allocates one spare
byte and absorbs the overread, so it is not the seam.

The fixture is a NIFTI-1 image carrying a CIFTI extension whose payload
is space-padded to hold no NUL. The overread is reported by the sanitizer
legs; the build stays green without them.
The tool writes both members of the pair itself, so no data is needed.
The driver asserts exit 1 and the diagnostic, because a library reports
the clash to its caller rather than ending the process, and a plain
add_test cannot tell that exit from the abort it replaced.

It then removes the .gz and repeats, so an unconditional NULL return
could not pass. Guarded on FSLSTYLE_NAME_CONFLICTS, which is off by
default, so the test is registered only where the code is compiled.
@hjmjohnson
hjmjohnson force-pushed the test/regression-coverage branch from fa0d6bf to 902db64 Compare September 22, 2026 12:19
Two paths returned without releasing the header, and one without
releasing the duplicated name: a write into a directory that refuses it,
and a duplication that fails because the input is the other NIfTI
version. Both are reachable from the shipped tools with no data beyond
what -make_im writes.

The script asserts only that the paths are reached and reported; the
leak is what the memcheck and sanitizer legs see. It skips itself for
root, whom a read-only directory does not stop.
c22_copy_image compares by content because gzip output is not
reproducible across zlib implementations, but that property was only
exercised where the system zlib is zlib-ng. Every CI leg ships stock
zlib, so the comparison could revert to comparing compressed bytes and
the suite would stay green.

Extracting the helper lets it be tested directly. gzip -1 against -9
manufactures the byte difference on any zlib, and the two negative
assertions pin that differing content is still reported as different and
that the uncompressed path still works.
@hjmjohnson
hjmjohnson force-pushed the test/regression-coverage branch from 902db64 to 2fe4b60 Compare September 22, 2026 12:27
@hjmjohnson
hjmjohnson marked this pull request as ready for review September 22, 2026 12:33
@hjmjohnson
hjmjohnson merged commit bcf1b73 into master Sep 22, 2026
21 checks passed
@hjmjohnson
hjmjohnson deleted the test/regression-coverage branch September 22, 2026 12:36
@hjmjohnson

Copy link
Copy Markdown
Member Author

The commit messages in this range were rewritten to remove trailers that do not belong in permanent history: Co-Authored-By: naming an AI tool, and Claude-Session: URLs that resolve for nobody. Only messages changed — the tree at the tip of master is byte-identical, and author, committer, and dates are preserved.

This PR's commits on the rewritten master:

  • 2f6742c274 ENH: Cover the NIFTI-2 dim[0] range check
  • e3990de7a2 ENH: Cover the swap width of a byte-swapped nifti_1_header
  • 3deb735135 ENH: Cover the swap width of the tool's own header copies
  • 01ce2fa27e ENH: Cover the voxel-count and volume-size overflow guards
  • 329e869448 ENH: Cover the voxel-count guards in the NIFTI-1 library
  • d0b9413781 ENH: Cover the repeated-attribute path in the ASCII header reader
  • 457f8bfef6 ENH: Cover the loc_strnlen bound on an unterminated extension
  • 7a0b98b2d7 ENH: Cover the ambiguous-filename path in nifti_findhdrname
  • c051fd89fc ENH: Reach nifti_tool's header-modification failure paths
  • b5023c5a98 ENH: Self-test the NIfTI content comparison helper
  • f79d2349ea BUG: Check the header converted to nifti_image before reading its datatype

The SHA recorded above by GitHub is from the pre-rewrite history and no longer resolves.

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.

1 participant