Re: alx-0096r1 - string (and nonstring) copying

Mark Harris <[email protected]> Wed, 29 Jul 2026 10:27:15 -0700
Newsgroups org.kernel.vger.linux-man
Message-ID <CAMdZqKE-ZkCWhH3yY7200qEYf7xtFQuhxsEi9jiLgnVU4gKoAQ@mail.gmail.com>
 Alejandro Colomar wrote:
> >  4.3 BSD (1986) then also added the mem*()
> > functions for System V compatibility.  Later the C89 committee chose
> > to standardize the mem*() functions but moved them from their own
> > header file memory.h (introduced by System V) to string.h with the
> > str*() functions.
>
> Wow!  This is very interesting.  So, after all, we shouldn't blame old
> Unix systems, but the C Committee!!  Another very important issue to add
> to the list of bad inventions of the committee.  :)
>
> I wish we could re-introduce the <memory.h> header, and move these back
> there (of course, with compat declarations in <string.h>).

Since you propose to add functions like memtostr() and strtomem(), I
would think that having them both in the same header file would be
beneficial.  Otherwise it would add yet another area of potential
confusion for users:  Do I need to include memory.h or string.h for
strtomem()?  Does it depend on the source or destination argument?  I
thought the goal was to reduce confusion.


> > That is, memtostr(str, mem, n), introducing a whole
> > new area of potential confusion that was not present in any existing
> > mem*() or str*() functions.  If reducing potential confusion is that
> > important I would have expected maybe strfrommem(str, mem, n) or
> > similar.
>
> Hmmmm, interesting suggestion!  That makes it resemble the strfromd(3)
> family, with which it actually has some (minor) relation.  (It might be
> a bit
>
> strfrommemcat() would be a weird name, because the cat is next to mem
> instead of str.  Should it be strcatfrommem()?

I think that strcatfrommem() is better than strfrommemcat().  Although
I am not convinced that it is worth changing the name of a
well-established 49-year-old function just because the name may
confuse some people, especially if it may end up just exchanging one
area of confusion for a different one that people haven't even yet
learned to watch out for.

> On the other hand, there's precedent in strtol(3) having the arguments
> reversed compared to the name.  Programmers are used to having the
> destination in the first parameter.

strtol() is fine, because str is first in the name and is also the
first argument.  What makes memtostr() confusing is that mem is
mentioned first in the name but it is the second argument, and str is
mentioned second but it is the first argument.


>
> [...]
> > >     shadow; strtcpy(), strncpy(3)
> > >         The shadow project more or less agrees with the Linux kernel in
> > >         this regard.  It has implemented strtcpy(), and uses strncpy(3).
> > >         It doesn't have memtostr() yet, because it happens to always
> > >         allocate the buffer at the same time, using strndup(3) --which
> > >         is essentially malloc(3)+memtostr()--, but that will change
> > >         soon, because (non-VLA) arrays are safer, as they allow
> > >         validation of source and destination sizes at compile time.
> > >
> > >         The implementations are independent, and happened to be
> > >         fundamentally identical.  It seems that good implementations of
> > >         string and nonstring copying code converge to this set of
> > >         interfaces.
> > >
> > >         Here's a naive implementation of strtcpy():
> > >
> > >                 ssize_t
> > >                 strtcpy(char *restrict dst, const char *restrict src, size_t dsize)
> > >                 {
> > >                         bool    trunc;
> > >                         size_t  dlen, slen;
> > >
> > >                         if (dsize == 0)  // UB
> > >                                 abort();
> > >
> > >                         slen = strnlen(src, dsize);
> > >                         trunc = (slen == dsize);
> > >                         dlen = slen - trunc;
> > >
> > >                         stpcpy(mempcpy(dst, src, dlen), "");
> > >
> > >                         if (trunc) {
> > >                                 errno = E2BIG;
> > >                                 return -1;
> > >                         }
> > >
> > >                         return slen;
> > >                 }
> >
> > ISO C does not have ssize_t or E2BIG.
>
> I'm aware; this is a push to add ssize_t.  In the wording, I have
> specified strtcpy() as not-necessarily setting errno, though, since it's
> not strictly necessary.
>
> >  Also this API does not handle
> > the case of slen too large for ssize_t, which may not be an issue for
> > Linux, however ISO C has to support a much wider range of systems.
> > The same issues apply to strtcat().
>
> I believe sizes beyond ssize_t are not valid.  At least, for a ssize_t
> that matches ptrdiff_t.  No object may be larger than PTRDIFF_MAX,
> because that would mean that pointer subtraction would be undefined:
>
>         char *first = a;
>         char *end = a + sizeof(a);
>
>         end - first;  // Overflows ptrdiff_t
>
> That's because pointer subtraction happens on ptrdiff_t instead of
> size_t.  Thus, we don't need to care about sizes larger than SSIZE_MAX.

ISO C allows objects to have a size up to SIZE_MAX (§ 6.2.5).  ISO C
does not limit objects to size PTRDIFF_MAX, it just says that the
pointer subtraction is undefined if the result is not representable in
an object of type ptrdiff_t (§ 6.5.7).  It is the responsibility of
your code to avoid undefined behavior.  Glibc malloc() deliberately
fails on sizes > PTRDIFF_MAX to help clients to avoid this undefined
behavior, but that is not an ISO C requirement and it is still
possible to obtain such an object in other ways.  Also some systems
(e.g. with segmented or banked memory models) have a ptrdiff_t that is
larger than size_t.


>
> [...]
> > >     System V; memccpy(3)
> > >         memccpy(3) was invented in System V (like the other mem*()
> > >         functions).  The System V sources are not public (AFAIK), so
> > >         it's not known what this function was designed for.  It was
> > >         later added to the BSDs and glibc for compatibility with SysV,
> > >         but nobody really knew what this function is good for.  It
> > >         doesn't seem ergonomic for any basic functionality.
> >
> > It is for implementing functions like fgets(), which needs to copy the
> > next '\n'-delimited line from a buffer filled by read() (so not a
> > null-terminated string).
>
> Hmmmm, do you have any such code?  I'd be interested in reviewing it.
> I still suspect it might have been better if it didn't copy the
> terminator.

Sure.
https://github.com/illumos/illumos-gate/blob/4b44494cae63d4bf0c7d3f34828503e68d1c0e69/usr/src/lib/libc/port/stdio/fgets.c#L44

>
> [...]
> > >     POSIX; memccpy(3)
> > >         POSIX standardized memccpy(3) just because it was in System V.
> > >         POSIX derives from Issue 1 of the SVID.  It's not surprising
> > >         that it's there.  Especially, this function was always in POSIX,
> > >         and the early revisions of POSIX are known to have strong
> > >         preference for SysV functions, regardless of their quality or
> > >         widespread use.
> > >
> > >         Interestingly, POSIX mentions that memccpy(3) does not check for
> > >         overflow.
> > >
> > >         > The memccpy() function does not check for the overflow of the
> > >         > receiving memory area.
> > >
> > >         This is because the 4th parameter to memccpy(3) is not the size
> > >         of the destination buffer, but the size of the source buffer.
> > >         It is assumed that the destination buffer is large enough.
> >
> > That is silly; the maximum size applies to both the source and
> > destination, like memcpy().  Obviously if one is smaller than the
> > other, you must not pass in a size that is larger than the smaller of
> > the two, as with memcpy().
> >
> > >
> > >         This hints that the original (System V) authors of the function
> > >         didn't consider copying strings as a use case for this function.
> >
> > It's a "mem" function, not a "str" function, so it should be clear
> > that it is intended for processing byte arrays and not null-terminated
> > strings.  That said, if you wanted to I guess you could use it to
> > implement a function like your strtcpy() pretty easily:
> >
> >     if (dsize == 0) abort();
> >     char *p = memccpy(dst, src, '\0', dsize);
> >     return p ? p-dst-1 : (dst[dsize-1] = '\0', -1);
>
> FWIW, I wouldn't call this 'pretty easily', but this is a bit
> subjective.  I'd possibly make bugs in the code above.

It is shorter than your "naive" strtcpy() implementation, and doesn't
depend on an undocumented non-C23 function.


 - Mark