From: Alejandro Colomar <alx@kernel.org>
To: Mark Harris <mark.hsj@gmail.com>
Cc: sergeh@kernel.org, linux-man@vger.kernel.org,
"Serge E. Hallyn" <serge@hallyn.com>,
"G. Branden Robinson" <g.branden.robinson@gmail.com>,
Douglas McIlroy <douglas.mcilroy@dartmouth.edu>
Subject: Re: alx-0096r1 - string (and nonstring) copying
Date: Tue, 28 Jul 2026 17:04:15 +0200 [thread overview]
Message-ID: <ami6Pbs0oT9gLi4W@devuan> (raw)
In-Reply-To: <CAMdZqKGFF-1fqsT0xSZDrBSGC-zztxayX3xAhFHXP-cKjEthpg@mail.gmail.com>
[-- Attachment #1: Type: text/plain, Size: 16149 bytes --]
Hi Mark,
Thank you very much for the thorough review!
On 2026-07-28T05:05:51-0700, Mark Harris wrote:
[...]
> > Seventh Edition Unix; strncpy(3)
> > strncpy(3) was first introduced in Seventh Edition Unix (V7).
> >
> > It was added for one very specific use case: copying a source
> > string into a destination character sequence in a fixed-size
> > member of a structure (not a string), and padding the unused
> > bytes with '\0'.
> >
> > This was useful bad then in members of the utmp(5) structure,
> > and modern shadow-utils still need this function for dealing
> > with utmp(5).
>
> It used to be much more common. For example, in V7 Unix there was
> only one filesystem type and no opendir()/readdir(); any user program
> wanting to read the contents of a directory would open() it for
> reading like any other file and read the sequence of 16-byte directory
> entries, each of which is a 2-byte inode number followed by a
> fixed-size 14-byte filename. A shorter filename was padded with null
> bytes at the end but there was no null if it was exactly 14 bytes (the
> maximum length). strncpy() could write this kind of fixed-length
> field, and strncat() could append such a filename to a path string.
Indeed. These days, cheap storage means these 1-byte savings are less
common. They still appear in some cases, but they're significantly less
common.
> At https://softwareengineering.stackexchange.com/questions/438025/what-was-the-original-purpose-of-c-strncpy-function/450802#450802
Hmmmm, thanks for the link! That's interesting.
> the original author (or so they claim) says that the original
> motivation was to handle the fixed length command name in process
> accounting records.
The claim seems credible. He gives some detail that seems likely true:
He says the strncpy(3) functions were previously called strcpyn().
While I can't verify this, there's the macro SCPYN(). I had always
wondered why they reversed the order in the name --the natural thing
would be to call it SNCPY()--. If the original strncpy(3) was called
strcpyn(), this would make sense.
[...]
> > System V; mem*() functions
> > Back in the times of V7, the concept of a string was less
> > specific, and what we now call byte arrays, they called byte
> > strings. In fact, memcmp(3) was then called bcmp(3). That's
> > why all the byte functions are provided in <string.h>.
> >
> > System V invented the mem*() functions --AFAIK--. They are
> > essentially the b*() funcitons from V7, with minor tweaks to the
> > parameters and return values. For example, memcpy(3) was made
> > consistent with strcpy(3), in that both take the input as $2
> > and write to $1. The old bcopy(3) was reversed compared to
> > strcpy(3), which was quite confusing and error-prone
> > (especially, before 'const' was invented).
>
> It was 4.2 BSD that introduced bcopy/bcmp/bzero in libc, the same year
> (1983) that System V introduced the mem*() functions in their libc.
> V7 Unix did not have any of these functions in libc, although it did
> have a bcopy() function in the V7 kernel so BSD may have been trying
> to be compatible with that.
Hmmm, I had seen bcopy() existing there in the source code, and didn't
imagine that it would be a private function for the kernel only.
I also had seen some bcmp():
alx@devuan:~/src/unix/unix/v7$ grep -rn bcmp
usr/src/cmd/sa.c:54: extern tcmp(), ncmp(), bcmp();
usr/src/cmd/sa.c:202: qsort(tab, k, sizeof(tab[0]), nflg? ncmp: (bflg?bcmp:tcmp));
usr/src/cmd/sa.c:345:bcmp(p1, p2)
And I guessed it was the bcmp(3) we know, but now I've checked the
implementation and it was something entirely different.
Thanks for the correction! I'll fix the paper.
> 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>).
> > POSIX says memcmp(3) was first standardized in Issue 1 of the
> > SVID (System V Interface Definition), which corroborates that
> > these functions were invented in SysV.
> >
> > It is good that such a renovation of the names, parameters, and
> > return values happened.
> >
> > It would have been good if strncpy(3) and strncat(3) would have
> > been renamed (and tweaked) too back then, but it didn't happen.
> >
> > C89; string
> > C89 specified the term 'string' very clearly, in 4.1.1:
> >
> > A string is a contiguous sequence of characters
> > terminated by and including the first null character.
> >
> > This differed from the old ambiguous meaning. The header file
> > for operations on byte arrays remained <string.h> for historical
> > reasons, even if it conflicted with this specification of
> > string.
>
> When System V introduced the mem*() functions they put them in a new
> header file memory.h. Also when 4.3 BSD added the mem*() functions
> for System V compatibility, they added a memory.h header file with
> these functions and did not add them to string.h. When C89 adopted
> these functions they moved them from memory.h to string.h; they did
> not "remain" in string.h since they did not start there. Modern Linux
> and macOS have a memory.h that includes string.h, for compatibility
> with code that may still include memory.h, even though no version of
> ISO C or POSIX has ever required a memory.h.
Thanks a lot for these details!! These are very important to understand
why this has been so wrong. I'll make sure to fix the paper to include
this history.
> > The strn*() functions, strncpy(3) and strncat(3), also remained
> > with their names, for historical reasons, even if it conflicted
> > with this specification of string.
> >
> > Other than that, the standard was quite consistent and strict
> > with its meaning of the term string.
> >
> > I believe this consistency --with the exceptions mentioned
> > above-- is what led programmers to trust string functions to
> > handle strings, and anything with a str*() in the name was
> > believed to be good for handling strings. Programmers forgot
> > the not-so-consistent history of the term string prior to
> > standardization.
> >
> > In retrospective, the strncpy(3) function would have been better
> > called strtomem_pad(), which clearly reflects what it does.
> > Maybe that would have made programmers aware of its semantics,
> > and it would have significantly reduces misuses of the function.
> >
> > strncat(3)
> > strncat(3) is similar to strncpy(3) --while at the same time,
> > very different--. It is actually the opposite of strncpy(3):
> > it takes a nonstring as input, and writes a string.
> >
> > A better name for it would have been memtostr_cat().
> >
> > Curiously, there's no 'cpy' version of strncat(3). There's
> > nothing with the semantics of memtostr() in ISO C, nor in the
> > historic Unix systems (V7, BSD, System V).
>
> I think the main source of confusion with strncat() is that for other
> str*() and mem*() functions that take a destination buffer and size
> argument, the size is the size of the destination buffer (i.e., it
> will not write to dst[size] or later), whereas for strncat() the size
> only limits how much is read from the source.
Yup. In retrospective, strn*() functions should be treated as a third
class of functions, entirely unrelated to str*() and mem*().
> So what I find more curious is that ISO C has no string concatenation
> function that takes a destination buffer size (such as your
> strtcat()).
I'd say this is reasonable, since it doesn't have strtcpy() either.
> If that is what a person is looking for, the plausible
> name and arguments and absence of other contenders can lead them to
> conclude that strncat() must be that function.
Yup.
[...]
> > Linux; strtomem_pad()
> > Recently, it appeared in the (fake) news that Linux had
> > completely removed all uses of strncpy(3), and banned it in new
> > code.
>
> Perhaps clarify that you are referring to the Linux kernel, not Linux glibc.
Okay.
> > ...
> > Thus, the functions that should be considered are:
> >
> > - strtcpy() (strscpy(9) in Linux)
> > - memtostr() (the 'cpy' of strncat(3))
> > - strtomem() (strncpy(3) renamed)
> >
> > And for consistency, strncat(3) should be renamed to something
> > like memtostrcat().
>
> Given that the entire point of strtomem() and memtostrcat() is to
> change a confusing name, I am surprised that the new names you came up
> with name mem and str in the opposite order that they apply to the
> argument list.
I derived the names from those invented by the kernel; they're not my
invention. I took their names because they were better than the ones
I had been using myself in my code. But I agree your suggestion seems
even better.
> 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()?
[...]
> > 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.
[...]
> > 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.
[...]
> > 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.
> Note that this just passes the destination buffer size directly to
> memccpy(); unless I missed something this should not allow the
> destination buffer to overflow.
Your implementation seems correct.
> I don't see sufficient justification for removing this function.
I'd like to review an fgets(3) implementation using memccpy(3) before
making a judgement.
Have a lovely day!
Alex
>
>
> - Mark
--
<https://www.alejandro-colomar.es>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
next prev parent reply other threads:[~2026-07-28 15:04 UTC|newest]
Thread overview: 31+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-02 12:33 [PATCH v1 1/2] man/man3/str*.3: NAME: Explain the names Alejandro Colomar
2026-07-02 12:33 ` [PATCH v1 2/2] man/man3/strpbrk.3: BUGS: Clarify the NAME Alejandro Colomar
2026-07-03 5:03 ` [PATCH v1 1/2] man/man3/str*.3: NAME: Explain the names Mark Harris
2026-07-03 11:16 ` Alejandro Colomar
2026-07-08 15:09 ` [PATCH v2 0/4] str*.3, mem*.3: " Alejandro Colomar
2026-07-08 15:09 ` [PATCH v2 1/4] man/man3/str*.3: " Alejandro Colomar
2026-07-24 18:52 ` sergeh
2026-07-25 22:09 ` Alejandro Colomar
2026-07-26 23:14 ` alx-0096r1 - string (and nonstring) copying Alejandro Colomar
2026-07-28 12:05 ` Mark Harris
2026-07-28 15:04 ` Alejandro Colomar [this message]
2026-07-28 15:58 ` Alejandro Colomar
2026-07-29 17:27 ` Mark Harris
2026-07-29 19:23 ` Alejandro Colomar
2026-07-30 17:58 ` Mark Harris
2026-07-30 19:51 ` Alejandro Colomar
2026-07-30 20:02 ` Alejandro Colomar
2026-07-27 2:39 ` [PATCH v2 1/4] man/man3/str*.3: NAME: Explain the names Serge E. Hallyn
2026-07-27 7:51 ` Alejandro Colomar
2026-07-08 15:09 ` [PATCH v2 2/4] man/man3/strpbrk.3: BUGS: Clarify the NAME Alejandro Colomar
2026-07-24 18:53 ` sergeh
2026-07-25 22:10 ` Alejandro Colomar
2026-07-08 15:09 ` [PATCH v2 3/4] man/man3/mem*.3: NAME: Explain the names Alejandro Colomar
2026-07-24 18:55 ` sergeh
2026-07-08 15:09 ` [PATCH v2 4/4] man/man3/[b]string.3: Rewrite and merge Alejandro Colomar
2026-07-25 23:49 ` [PATCH v3 0/4] str*.3, mem*.3: NAME: Explain the names Alejandro Colomar
2026-07-25 23:49 ` [PATCH v3 1/4] man/man3/str*.3: " Alejandro Colomar
2026-07-25 23:50 ` [PATCH v3 2/4] man/man3/strpbrk.3: BUGS: Clarify the NAME Alejandro Colomar
2026-07-25 23:50 ` [PATCH v3 3/4] man/man3/mem*.3: NAME: Explain the names Alejandro Colomar
2026-07-25 23:50 ` [PATCH v3 4/4] man/man3/[b]string.3: Rewrite and merge Alejandro Colomar
2026-07-29 15:15 ` [PATCH v3 0/4] str*.3, mem*.3: NAME: Explain the names Alejandro Colomar
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ami6Pbs0oT9gLi4W@devuan \
--to=alx@kernel.org \
--cc=douglas.mcilroy@dartmouth.edu \
--cc=g.branden.robinson@gmail.com \
--cc=linux-man@vger.kernel.org \
--cc=mark.hsj@gmail.com \
--cc=serge@hallyn.com \
--cc=sergeh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.