Skip to content

WIP: Replace dangerous old string functions with less horrible variants, for -fbounds-safety compatibility - #24

Draft
seanm wants to merge 77 commits into
InsightSoftwareConsortium:masterfrom
seanm:kill-old-str-fns
Draft

seanm wants to merge 77 commits into
InsightSoftwareConsortium:masterfrom
seanm:kill-old-str-fns

Conversation

@seanm

@seanm seanm commented Jan 14, 2026

Copy link
Copy Markdown
Collaborator

@hjmjohnson this one needs careful checking.

Also it's not done yet, I have to do some cmake stuff to compile the new file.

@gdevenyi

gdevenyi commented Aug 15, 2026 •

Copy link
Copy Markdown
Closed PR comment

Heads up that #26 will conflict with this one, and on a couple of details that may be worth folding in whenever this comes off WIP.

The conflicts: #26 rewrites the QSTR macro in both nifti1_io.c and nifti2_io.c (parenthesising sz), rewrites nifti_findhdrname() in both files, and removes nifticdf entirely — so the nifticdf.c hunk here disappears along with the file. Worth noting that this PR's rewrite of nifti_intent_code() drops the malloc NULL check that #23 adds to the same function.

Two things #26 found in code this PR touches:

nifti_findhdrname() under -DFSLSTYLE has a use-after-free — basename is freed, then read by the fprintf reporting the ambiguous-filename error — plus an unchecked calloc whose result goes straight into strcpy/strcat. Both are in the block right above the exit(134).

On string_helper/: the guard is #if defined(__GLIBC__) && ..., so on musl, MSVC, or any non-glibc non-Apple libc __GLIBC__ is undefined, the fallback is not compiled, and strlcpy is missing at link time. Also, defining strlcpy/strlcat at global scope adds symbols with libc-reserved names to the library, which can collide for downstream consumers — a nifti_strlcpy would avoid that. And strlcat(elist[0], extgz, 8) uses a hardcoded 8 rather than sizeof.

Not asking you to rebase across #26 — if it lands first I am happy to do that rebase myself.

@gdevenyi

gdevenyi commented Aug 15, 2026 •

Copy link
Copy Markdown
Closed PR Comment

Followed up on this properly rather than leaving it at "conflicts with #26". I audited every strcpy/strcat call in the tree to work out what the wholesale replacement would actually buy, and the answer surprised me.

Two are genuine defects, and #26 now fixes both.

doPigz()/doPigz2() build a shell command in char command[768]:

strcpy(command, "pigz");
strcat(command, " -n -f > \"");
strcat(command, nim->fname);      /* unbounded */
strcat(command, "\"");

PATH_MAX is 4096, so an ordinary deep path overruns a 768-byte stack array and the result goes to popen(). #11 grows the buffer to 4096, which does not fix it — 4096 is exactly PATH_MAX, and the prefix and quoting push a maximum-length path over. #26 replaces all four calls with one bounded snprintf that refuses to run a truncated command.

znzprintf() had the tree's only unbounded formatted write — vsprintf into a buffer sized strlen(format) + 1000000, with the comment /* overkill I hope */. Now vsnprintf, and the va_list is ended on the allocation-failure path that previously returned without va_end.

The other 145 calls are correct, which is why #26 does not rewrite them:

  • 86 copy into a buffer allocated as strlen(source) + slack a few lines above — the nifti_makehdrname/nifti_makeimgname family and nifti_strdup.
  • 10 write into a fixed array that fits exactly: extcopy[8] receives name + len - 7, the elist[] entries are the char[8] extension buffers whose declaration comment says "(leave space for .gz)", nhdr.magic[4] takes a 3-character literal.
  • The rest are string literals into struct fields many times their length.

Rewriting 145 correct calls is 145 chances to get a length expression wrong, in code whose tests compare output byte for byte. I hit exactly that failure mode earlier in this work: a regex that looked right turned calloc(strlen(basename) + 8, sizeof(char)) into calloc(strlen(basename, sizeof(char)) + 8), and only the binary-comparison test caught it.

So #26 takes a different route to the same goal: _FORTIFY_SOURCE, level chosen by compiling a test rather than checking versions, applied only to optimised builds. The C library then checks the destination size of every one of those calls wherever the compiler can determine it — including the run-time-known strlen(x) + n sizes, which is what level 3 adds and what this codebase needs. I confirmed it catches the doPigz overflow above.

This complements what you are doing rather than replacing it. Fortification catches a bad size at run time; strlcpy and -fbounds-safety make the bound explicit at compile time, which is strictly better where you can get it. If this PR lands, the fortification stays useful.

Two things about the current implementation that would need sorting either way, repeating them here so they are not lost:

  • string_helper.h guards the fallback with #if defined(__GLIBC__) && ..., so on musl, MSVC, or any non-glibc non-Apple libc __GLIBC__ is undefined, the fallback is not compiled, and strlcpy is missing at link time.
  • Defining strlcpy/strlcat at global scope claims identifiers reserved to the implementation, which can collide for downstream consumers. A nifti_strlcpy would avoid that, at the cost of not being a drop-in.

Happy to rebase #26 under this if you would rather land yours first — the two touch different lines apart from the doPigz block and the QSTR macro.

@seanm

seanm commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

@gdevenyi your LLM seems to be unaware that with -fbound-safety strncpy() and strcpy() do not compile at all, and that is why I've changed them to strlcpy (as per the commit message "The old functions can easily overflow buffers but also they are not allowed under -fbounds-safety at all.", which I guess the LLM did not parse. It has missed the point entirely.

Instead of making it redo everything I worked hard to do, could you instead use it to review the correctness of this PR? It seems to think 145 calls were correct, does it think they are still correct after the changes here?

@gdevenyi gdevenyi left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Claude Fable with effort high

Thanks for tackling this — the call-site conversions all look correct. I checked each fixed buffer against its declaration: extcopy[8] holds the 7-char extensions, extnii[8] + ".gz" fills exactly 8 with the NUL, magic[4] holds "n+1\0", and the strlen(...)+8 allocations cover extension + ".gz" + NUL. The QSTR macro change correctly adds memset to preserve the zero-padding strncpy used to provide.

The blockers are all in the new string_helper fallback (see line comments):

  1. The platform guard only compiles the fallback on glibc < 2.38, so MSVC/MinGW (no __APPLE__, no __GLIBC__, no strlcpy) get no definition at all and every call fails to link. Suggest check_symbol_exists(strlcpy string.h HAVE_STRLCPY) in CMake instead of enumerating platforms.
  2. string_helper.h tests __GLIBC__ before including any libc header, so the prototypes vanish if the header is included first.
  3. The strlcat fallback is unsafe under NDEBUG in a case where BSD's real strlcat is safe.

Also noting (you already flagged the CMake wiring as unfinished): none of the modified .c files #include "string_helper.h" yet, so on pre-2.38 glibc the calls will be implicit declarations even once the file is compiled. Minor: the new files use tabs, // comments, and camelCase parameter names, which don't match the surrounding codebase.

#ifndef __APPLE__

// libc 2.38 and above provides strlcpy and strlcat, so don't redefine them.
#if defined (__GLIBC__) && ((__GLIBC__ < 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ < 38)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard only provides the fallback when __GLIBC__ is defined and < 2.38. On MSVC/MinGW neither __APPLE__ nor __GLIBC__ is defined, so the whole block preprocesses away — and MSVC has no strlcpy, so every call site fails to link. This repo supports MSVC (there are _MSC_VER branches in doPigz).

Rather than enumerating platforms, detect the symbol in CMake:

include(CheckSymbolExists)
check_symbol_exists(strlcpy string.h HAVE_STRLCPY)

and guard on #ifndef HAVE_STRLCPY. That also handles musl, the BSDs, and future libcs without further edits.

#ifndef __APPLE__

// libc 2.38 and above provides strlcpy and strlcat, so don't redefine them.
#if defined (__GLIBC__) && ((__GLIBC__ < 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ < 38)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__GLIBC__ is only defined after a libc header has been included. If a .c file includes string_helper.h before <string.h>, this check sees __GLIBC__ undefined and the prototypes vanish even on old glibc. Add #include <string.h> at the top of this header, before the version check (or better, switch to a CMake HAVE_STRLCPY guard as suggested in the .c file).


#include <stddef.h>

size_t strlcpy(char* restrict ioDestination, const char* restrict inSource, size_t inDestinationSize);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restrict is not a keyword in C++, and there's no extern "C" guard, so any C++ consumer (e.g. ITK) that includes this header will fail to compile. Wrap the declarations in extern "C" and hide restrict behind #ifdef __cplusplus.

assert(destinationLength < inDestinationSize);

ioDestination += destinationLength;
inDestinationSize -= destinationLength;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the destination is not NUL-terminated within inDestinationSize, the assert on line 41 is compiled out under NDEBUG, and this subtraction underflows to a huge value. The truncation branch below is then skipped and the memcpy writes past the buffer. OpenBSD's strlcat handles this case safely by returning size + strlen(src) without writing:

if (destinationLength >= inDestinationSize)
    return inDestinationSize + sourceLength;

All call sites in this PR are fine today, but a safety shim that can overflow where the real strlcat cannot defeats the purpose.

Comment thread niftilib/nifti1_io.c
strcat(command, "\"");
strlcpy(command, "pigz", sizeof(command));
strlcat(command, " -n -f > \"", sizeof(command));
strlcat(command, nim->fname, sizeof(command));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change worth handling: the old strcat overflowed on a very long fname (~750+ chars); strlcat now silently truncates instead, which drops the closing quote and hands pigz a mangled command. Since strlcat returns the total length it tried to create, check it against sizeof(command) and fail (return -1) on truncation. Applies to all four copies (doPigz/doPigz2 in both nifti1_io.c and nifti2_io.c).

Comment thread niftilib/nifti1_io.c
strcat(elist[0], extgz); strcat(elist[1], extgz); strcat(elist[2], extgz);
strlcat(elist[0], extgz, 8);
strlcat(elist[1], extgz, 8);
strlcat(elist[2], extgz, 8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded 8 is correct today (extnii[8] + ".gz" fills it exactly), but sizeof(extnii) / sizeof(exthdr) / sizeof(extimg) would stay correct if the buffers ever change. Same in the nifti2_io.c copy.

This was referenced Aug 15, 2026
gdevenyi and others added 4 commits September 22, 2026 10:28
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.
znzread(), znzwrite() and nifti_read_buffer() all return size_t and
all returned -1 to report an error.  In a size_t that value is
SIZE_MAX, larger than any length a caller can have asked for, so a
caller that tests the result with '<' reads the error as a complete
transfer:

    ii = nifti_read_buffer(fp, nim->data, ntot, nim);
    if( ii < ntot ){ ... }          /* SIZE_MAX < ntot is false */

The visible effect is that a truncated image loads as if it were
whole: nifti_image_read() prints its short-read warning, ignores the
failure and returns an image whose tail is uninitialized heap.
FslReadVolumes() has the same problem one level up, dividing the
returned byte count by the volume size to report SIZE_MAX/volbytes
volumes read.

These now return 0, which every one of these callers already treats
as failure and which is the only value a size_t function has for
"nothing was transferred".  The published return types in znzlib.h
and nifti1_io.h are unchanged.

The nifti2 equivalents return int64_t, where -1 is representable,
and are left alone.
Writes a 31x31x31 float image, copies it 1000 bytes short, and asserts
nifti_image_read() returns NULL for the copy and a populated image for
the original, so a blanket refusal cannot pass.  Without the fix the
truncated file comes back accepted with nvox=29791 and an
uninitialized tail.
gdevenyi and others added 22 commits September 22, 2026 11:23
afni_xml's parser holds sixteen levels of open elements in a fixed
array and refuses to push a seventeenth by entering a skip block:
epush() records the depth in xd->dskip and stops touching the stack
until the matching pop.  Two details of that bookkeeping are wrong.

epush() overwrote dskip on every push past the limit, so the skip was
recorded as starting at the innermost depth.  epop() then cleared
dskip before the stack was handled, so the pop that ends the skip fell
through to "xd->stack[xd->depth-1] = NULL" and wrote a pointer eight
bytes past the sixteen-entry array, which lives in the file-scope
afni_xml_control.  The next pop found dskip clear and dereferenced
xd->stack[16].

epush() now keeps the outermost skip depth, since that is the depth
whose pop ends the skip, and epop() clears dskip only after the
element has been skipped.  Depths within the limit are unaffected.

The XML comes from the CIFTI extension of a NIfTI file, or from any
file read by axml_read_file(), so the nesting is chosen by the input.
Found by fuzzing axml_read_buf() with libFuzzer under
AddressSanitizer.
process_popped_element() read xd->stack[xd->depth-1] and dereferenced
it without a test.  A slot is filled by the matching epush(), and an
element that was skipped never fills one, so the read is only safe
because the preceding commit keeps the skip running to the element
that started it.

Guard it rather than rely on that: check the depth is inside the
stack, and that the slot holds a struct with a name, before the
strcmp().
deep_nesting.xml nests twenty elements inside a CIFTI extension whose
stack holds sixteen, which is the shape that walked off the end of
xd->stack.  The test drives it through cifti_tool and pins that the
BrainModel sibling is still summarized, so a fix that refused the
whole document would not pass.
A branch filter naming a branch that does not exist leaves the workflow
configured but never run, which looks the same as one that runs and
passes: no red check appears because no check appears at all. build.yml
sat dead behind a filter naming main on a repository whose branch is
master, and the only thing that found it was reading the file.

The check judges a filter only when every entry is a literal name, so a
release-* pattern or a branch that does not exist yet is left alone, and
it reports a name that resolves to nothing rather than one that merely
differs from the default.
cppcheck 2.19 nullPointerOutOfMemory: the uppercase copy of the name
was written into an unchecked malloc result.  Return -1, the same
value the function already uses for an unrecognized name.
cppcheck 2.19 unreachableCode: FSLIOERR ends in exit(EXIT_FAILURE), so
the return statements following it in FslReadAllVolumes and
FslReadHeader can never run.
Fixes some clang-tidy bugprone-macro-parentheses warnings.

Would be an issue where an argument that is an expression would evaluated different with different precedence.
Fixes many bugprone-implicit-widening-of-multiplication-result warnings.

Here the multiplications were happening in small types (int, usually 32 bit) then stored in large types (size_t, usually 64 bit). The multiplication could have overflowed. Now the multiplication is done with large types and thus less likely to overflow.
disp_cifti_extension() searched for the CIFTI extension with

    ext = nim->ext_list;
    for( ind = 0; ind < nim->num_ext; ind++ )
       if( ext->ecode == NIFTI_ECODE_CIFTI ) break;

ext is never advanced, so this tests the first extension num_ext times.
cifti_tool could only ever find a CIFTI extension that happened to be
first in the list; with any other extension ahead of it the tool
reported 'no CIFTI extension' for a file that has one.  It now indexes
ext_list[ind] and leaves ext NULL when there is no match, which also
avoids the read past the end of the list that a bare ext++ would have
introduced.

The same function opened its output stream before the 'no CIFTI
extension' check and returned without closing it; the early return now
closes the stream like the normal path does.
cext_second_extension.nii carries a comment extension ahead of the
CIFTI one.  The test requires the extension's payload in the output and
forbids the 'no CIFTI extension' message, and the existing
unterminated-cext test still pins the first-position case, so neither a
search that always matches nor one that never does would pass.
cdfbin() validates its `which` selector like this:

    if(!(*which < 1 && *which > 4)) goto S30;

A value cannot be both less than 1 and greater than 4, so the condition
is always false, the negation is always true, and the jump to S30 is
always taken -- skipping the entire range check.  gcc reports it as
-Wlogical-op, "logical 'and' of mutually exclusive tests is always
false".

The ten sibling functions in this file all spell the same guard with
`||`:

    cdfbet  1527:  if(!(*which < 1 || *which > 4)) goto S30;
    cdfchi  2255:  if(!(*which < 1 || *which > 3)) goto S30;
    cdfchn  2553:  if(!(*which < 1 || *which > 4)) goto S30;
    ... 7 more

so this is a single-character typo rather than an intentional deviation.

The effect is visible from the public API.  cdfbin is declared in the
installed nifticdf.h, and its contract is that an out-of-range input sets
*status to -1 and *bound to the limit that was violated.  Calling it with
which = 9:

    before:  status=999  bound=-999     (both left as the caller set them)
    after:   status=-1   bound=4        (the documented error return)

Before the fix the caller has no indication anything was wrong and the
function proceeds to compute with an unhandled selector.
The range check is reachable from the installed nifticdf.h, so assert
the documented contract directly: which=9 and which=0 must set
*status to -1 and *bound to the limit that was violated.  A which=1
call is asserted to still return status 0, so a guard that rejected
everything would not pass.
nifti_image_from_ascii() scans its input three times with

    ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ;
    if( ii == 0 || strcmp(lhs,"<nifti_image") != 0 ) return NULL ;

Two things are wrong with that pair of lines.  sscanf() returns EOF,
not 0, when the input ends before anything is matched, so ii == 0 does
not detect the failure; and nn is added to spos before ii is looked
at, so the position advances by an indeterminate amount.

For a string holding nothing but whitespace the first scan returns
EOF, the test passes, and strcmp() reads the uninitialized 1024 byte
stack buffer.  MemorySanitizer:

    WARNING: MemorySanitizer: use-of-uninitialized-value
        #0 nifti_image_from_ascii nifti2_io.c:8836

All three now test for the one successful conversion and only then
advance spos, which is what the surrounding code assumed.  The two
inside the loop cannot fail today, because whitespace is skipped and
end of string is checked before each, and the in-tree readers gate on
has_ascii_header(), so the first is reached only through a direct call
to this published function.  The same three lines exist in
nifti1_io.c and are corrected there too.

Found by fuzzing nifti_image_from_ascii() with libFuzzer under
MemorySanitizer.
19 -Wcast-align warnings, and behind them undefined behavior on any
target that cares about alignment.

modify_field() writes a value into a header field at a byte offset
parsed from a field table:

    ((short *)((char *)basep + field->offset))[fc] = (short)val;

field->offset is a byte offset into a packed on-disk header, so that
cast produces an address only correctly aligned by coincidence.  The
same pattern appears for int, int64_t, float and double, in both tool
files.  Each becomes a memcpy of the right width at the right byte
offset.

The second group reads a pointer back out of a structure through a
byte offset -- `sp = *(char **)((char *)str + fp->offset)` and the
nifti1_extension equivalents -- and becomes a memcpy into an aligned
local.

The third is nifti_header_version(), which cast its `const char * buf`
argument, a buffer straight off a file read with no alignment
guarantee, to both nifti_1_header * and nifti_2_header * and read
fields through them.  It now copies into aligned locals first, exactly
the sizeof(nifti_1_header) bytes the function already checks are
present.

Verified by round-tripping int16, int32, int64, float32, float64 and
string fields through nifti_tool -mod_hdr2.
Nine -Wcast-qual warnings.  Casting away const is how a read-only
contract turns into a write to memory the caller thought was safe, so
each was looked at rather than silenced.

fslio.c, FslGetFileType2()
    mutablefslio = (FSLIO *)fslio; /* dodgy and will generate warnings */
    mutablefslio->niftiptr->nifti_type = ...;

    The comment is right that it looks dodgy, but the cast was never
    needed.  `const FSLIO * fslio` makes the *member* niftiptr const --
    its type is `nifti_image * const` -- while what it points at stays
    fully mutable.  The assignment is legal as written, so the cast and
    the local both go.

fslio.c FslWriteVolumes(), cifti axio_num_tokens(), text_to_i64(),
text_to_f64()
    Pointers cast to char * and then only read: walked, indexed, or
    handed to strtoll()/strtod(), which take const char *.  Declared
    const, casts removed.

cifti strip_whitespace()
    Returned `(char *)str` on its early-exit paths, handing the caller a
    writable pointer to the const string it passed in.  The function is
    static, so its return type is nobody else's business; it now returns
    const char *.
afni_xml_tool.c and cifti_tool.c each define a file-scope `opts_t gopt`
with external linkage, so the two share a symbol name and neither
declares it in a header.  Each is used only inside its own translation
unit, always by address.

Both files build executables rather than libraries, so `static` removes
nothing from any shared library's exported symbols.

Clears both -Wmissing-variable-declarations warnings.
The build instructions in README.md were two lines about 'make all',
which builds a subset of the tree with a Makefile nothing else in the
project uses.  They now describe the CMake build the CI and the install
rules actually use, and note that the Makefile is unmaintained and does
not cover nifti2 or cifti.

A new section records how to run the sanitizers and valgrind, including
three things that each cost an afternoon to work out:

  * valgrind's memcheck needs --trace-children=yes here, because most of
    the tests are shell scripts that exec the tools.  Without it valgrind
    inspects the shell, sees nothing and reports a clean run;
  * ctest -T memcheck exits 0 even when valgrind reports defects, so the
    logs have to be read;
  * valgrind refuses to start without the C library's debug symbols, and
    on distributions that ship a stripped ld.so with no debuginfo package
    -- Arch and its derivatives -- it cannot be run at all, DEBUGINFOD_URLS
    included, because those builds are not on any debuginfod server.  A
    container recipe is given.

Every command in the new sections was run against this tree.
…plicit

152 of the -Wsign-conversion findings are an int or int64_t count
reaching a size_t parameter of malloc(), calloc(), realloc(), memcpy(),
memset(), strncpy(), znzread(), znzwrite() or fread().

Where a count is multiplied by a sizeof, the cast goes on the count
rather than around the product, so the multiply happens at 64-bit width:
(int)count * sizeof(T) can overflow before it is widened.

The counts cannot be negative. nifti_update_dims_from_array() clamps
every dim[i] to at least 1, nifti_datatype_sizes() leaves nbyper at 0
for an unknown datatype, and nifti_image_load() requires nbyper > 0 and
nvox > 0.
The findings that are not allocation, copy or I/O lengths: file offsets,
extension bookkeeping, and the ANALYZE orientation byte.

znztell() returns a signed znz_off_t stored in size_t locals, so the
znzseek() arguments are cast to znz_off_t and the arithmetic happens at
the width the function takes. fslio's nvox is widened per operand so the
product of seven int dimensions is computed at 64 bits. XML_Parse() takes
an int length while blen is unsigned; the conversion is now at the call.

The ANALYZE orientation byte is read through unsigned char * rather than
a signed lvalue. That is value-preserving either way -- signed to
unsigned char is defined modulo 256 -- so it states the intent only.
One change is a fix. nifti_read_buffer() passed
(int)(ntot / nim->swapsize) to nifti_swap_Nbytes(), which takes a size_t
count that was widened in 2010 because it "might not fit as int". The
cast reinstated that truncation, leaving most of an image above 2 GiB
unswapped. The cast is gone.

nifti_image_read() took the file size through (size_t), which turns
nifti_get_filesize()'s -1 error return into SIZE_MAX and slips past the
guard below it. A signed temporary is used instead, matching what
nifti2_io.c already does at the same place.

The rest casts an int dimension to size_t at the point of use.
The old functions carry no destination bound and are rejected outright
under -fbounds-safety.  Each call site was converted to snprintf,
strlcpy or strlcat with an explicit destination size.

strncpy zero-fills the whole destination and strlcpy does not; the call
sites relied on that only where the buffer came from calloc, except one
that now zeroes explicitly.  A few declarations moved to first use.
strlcpy and strlcat originate in OpenBSD and are now in the other BSDs,
macOS, and glibc from 2.38.  Supply implementations for the platforms
that still have neither.
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.

3 participants