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: 28+ 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-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox