65 Strings and memory — <string.h>
What to know first
Looking back
Chapter 42 said a C string is “up to the NUL”, so its length must be counted every time, and chapter 62 said functions that do not take a size are the first chronic illness. Then does using strncpy instead of strcpy cure that illness?
A. It does not. Contrary to the impression the name gives, strncpy was not designed as a safe copying function. Its original purpose was filling the fixed-length fields of old Unix (a 14-byte directory entry name, say) — so it fills all the spare places with zeros, and if there is not enough room it does not attach a NUL. Both properties go against the expectations of “string copying”. This chapter’s first example shows it before your eyes.
The need for this chapter, and its context
strncpy dangerous?” ends at “use it carefully”.By the end of this chapter
strncpy which is not safe despite its name, copying that touches overlapping regions, strtok which destroys the original and hides state — the dangers of strings learned in chapter 42 take concrete shape here function by function. We also see the real portability of the non-standard alternatives (the strlcpy family).The questions this chapter answers
- I hear
memcmpis dangerous for comparing passwords too?
65.1 The truth about strncpy
examples-en/ch65/strncpy.c
#include <stdio.h>
#include <string.h>
static void dump(const char *label, const char *buf, size_t n)
{
printf("%-14s", label);
for (size_t i = 0; i < n; i++) {
unsigned char c = (unsigned char)buf[i];
if (c == 0) printf(" \\0");
else if (c >= 32) printf(" %c", c);
else printf(" %02x", c);
}
printf("\n");
}
/* Put it behind a function boundary so the compiler cannot see the source's
length. (Otherwise gcc catches it with -Wstringop-truncation.) */
static void copy_into(char *dst, size_t cap, const char *src)
{
strncpy(dst, src, cap);
}
int main(void)
{
/* (1) it fills every spare place with 0 — expensive on a large buffer */
char pad[10];
memset(pad, 'X', sizeof pad);
copy_into(pad, sizeof pad, "abc");
dump("short source:", pad, sizeof pad);
/* (2) if it fits exactly or overflows, no NUL is attached — it stops being a string */
char tight[4];
memset(tight, 'X', sizeof tight);
copy_into(tight, sizeof tight, "abcd");
dump("exact fit:", tight, sizeof tight);
printf(" there is no NUL — printing this with %%s is outside the contract\n");
/* (3) to use it safely, close the last place by hand */
char safe[4];
copy_into(safe, sizeof safe - 1, "abcd");
safe[sizeof safe - 1] = '\0';
printf("closed by hand : [%s]\n", safe);
/* (4) to know whether it was truncated, the length must be measured separately after all */
const char *src = "abcd";
printf("truncated? : %s\n", strlen(src) >= sizeof safe ? "truncated" : "whole");
return 0;
}
Output
short source: a b c \0 \0 \0 \0 \0 \0 \0
exact fit: a b c d
there is no NUL — printing this with %s is outside the contract
closed by hand : [abc]
truncated? : truncated
Two things come out.
First, it fills all the spare places with zeros. Put 3 characters into a 10-byte buffer and it writes zeros over the remaining seven. The bigger the buffer, the bigger the waste.
Second, when it fits exactly or overflows it does not attach a NUL. Put abcd into a 4-byte buffer and there is no room for the NUL, so what remains is a byte array, not a string. Print that with %s or pass it to strlen and it reads outside the buffer — the typical route of “I used the safe function and it blew up.”
gcc really does catch this mistake. Here is the diagnosis received when first writing the example.
error: ‘strncpy’ output truncated before terminating nul copying 4 bytes
from a string of the same length [-Werror=stringop-truncation]But as seen in chapter 62, the compiler catches only what it can see. If the source’s length is settled during execution, this warning does not appear.
Counter-example. Copying “safely” with strncpy
char dst[32];
strncpy(dst, src, sizeof dst); /* there may be no NUL */
printf("%s\n", dst); /* it reads outside the buffer */It must be mended at least like this.
strncpy(dst, src, sizeof dst - 1);
dst[sizeof dst - 1] = '\0'; /* close it by hand */
if (strlen(src) >= sizeof dst) { /* truncated — handle it */ }Three lines are needed, and leaving out even one of them is an accident. That is why this function is assessed as “a safety device that is hard to use.”
65.2 Then what is used
Within the standard, the most practical tool for safely joining strings is in fact in <stdio.h>.
int need = snprintf(dst, sizeof dst, "%s", src);
if (need < 0 || (size_t)need >= sizeof dst) { /* truncated */ }There is the criticism that it is slow (the cost of interpreting the format), but it is the only standard function that keeps the boundary while letting you know about truncation.
| function | status | boundary | can truncation be known |
|---|---|---|---|
strcpy, strcat | standard | none | — |
strncpy | standard | yes | no (must be measured by hand) |
strncat | standard | yes (but the argument is the remaining room) | no |
snprintf | standard | yes | yes (the return value) |
strlcpy, strlcat | the BSD family, a C23 annex | yes | yes (the return value) |
strcpy_s, strcat_s | C11 annex K (optional) | yes | yes (an error return) |
Table 66.1
strncat’s argument is a particular trap — the second argument is not the destination’s size but the number of bytes that may additionally be written. strncat(dst, src, sizeof dst) is almost always wrong, and sizeof dst - strlen(dst) - 1 is right.
In practice. Why strlcpy was not standard
OpenBSD put out strlcpy and strlcat in 1998. They take the destination’s size, always close with a NUL, and return the length of the source so that truncation can be known. They spread through the BSD family and several libraries, but glibc long refused to adopt them — the counter-argument being that “an API that quietly permits truncation only moves the problem.”
So code using strlcpy was long unportable on Linux, and every project came to have its own edition. In 2023 glibc 2.38 finally added them and C23 brought in functions of similar intent as an annex, but the state in which you must check the target platform’s edition before saying “it can be used” persists. It is a representative case showing the gap between the standard and reality.
65.3 Overlapping regions — memcpy and memmove
examples-en/ch65/overlap.c
#include <stdio.h>
#include <string.h>
/* memcpy does not allow overlapping regions. memmove does. */
static void show(const char *label, const char *s)
{
printf("%-22s [%s]\n", label, s);
}
int main(void)
{
char buf[32];
/* a copy that does not overlap — the place where memcpy fits */
strcpy(buf, "abcdefgh");
char other[32];
memcpy(other, buf, strlen(buf) + 1);
show("no overlap (memcpy):", other);
/* shifting by one — source and destination overlap, so memmove is used */
strcpy(buf, "abcdefgh");
memmove(buf + 1, buf, 7); /* shift one place back */
buf[8] = '\0';
show("shift by 1 (memmove):", buf);
/* pulling forward overlaps as well */
strcpy(buf, "abcdefgh");
memmove(buf, buf + 2, 6 + 1); /* pull two places forward */
show("pull by 2 (memmove):", buf);
/* strtok destroys the original — and hides state inside the function */
char line[] = "name,age,city";
printf("before strtok : [%s]\n", line);
for (char *t = strtok(line, ","); t; t = strtok(NULL, ","))
printf(" token: [%s]\n", t);
printf("after strtok : [%s] <- the original has been cut\n", line);
return 0;
}
Output
no overlap (memcpy): [abcdefgh]
shift by 1 (memmove): [aabcdefg]
pull by 2 (memmove): [cdefgh]
before strtok : [name,age,city]
token: [name]
token: [age]
token: [city]
after strtok : [name] <- the original has been cut
memcpy’s contract includes “the two regions must not overlap”. Calling it with them overlapping is undefined behaviour, and in an optimised implementation values really do get scrambled — because there is no guarantee that bytes are moved in order (several bytes may be moved at once with SIMD, or moved from the back).
If they may overlap, it is memmove. Contrary to the impression its name gives, it does not mean “moving” but copying that is safe even when overlapping.
A common misconception. “memcpy is always faster than memmove”
memcpy can be faster is that it can use the premise “they do not overlap” in optimisation, and if that premise is set wrongly, what is lost (a bug that is hard to find) is far greater than what is gained (a few nanoseconds). If there is the slightest possibility of overlap, memmove — that is the modern default.65.4 strtok — it destroys the original and hides state
The last part of the example. strtok has two sins.
First, it destroys the original. It makes tokens by overwriting the separators with NUL. So it cannot be used on a read-only string (a string literal) — using it there is outside the contract — and if the original is needed it must be copied first.
Second, it hides state inside the function. That is why NULL is passed from the second call onward. That state is singular, so if another function calls strtok in the middle of cutting tokens the two wreck each other’s traversal. In a program running along several strands it gets worse.
There are three alternatives. Use an edition in which the caller holds the state, such as strtok_r (POSIX) or strtok_s (annex K); cut it yourself with strcspn and strchr; or use a tool that does not touch the original, like Part XII’s view-based splitting.
65.5 The traps of the remaining functions
| function | what it does | to beware of |
|---|---|---|
strlen | length | with no NUL it runs away. every time |
strcmp | comparison in dictionary order | only the sign of the return value means anything. 0 is “equal” |
strncmp | compare the first n bytes | if n exceeds the length it stops at the NUL |
strchr, strrchr | find a character | if the sought character is '\0' it points at the end |
strstr | substring | worst-case performance differs by implementation |
strspn, strcspn | length by a set of characters | the heart of the cutting idiom |
memset | fill with a byte | ★ for erasing secrets it may vanish under optimisation |
memcmp | compare bytes | ★ it compares padding too. it must not be used to compare structs |
Table 66.2
The two starred entries are especially dangerous in practice.
Erasing a secret with memset — the memset(key, 0, len) that erases after use may, if key is not read afterwards, be seen by the compiler as a “useless write” and deleted (chapter 13′s optimisation story). C11 put memset_s in annex K for this, and each platform has a function such as explicit_bzero or SecureZeroMemory.
Comparing structs with memcmp — because of the padding seen in chapter 46. Even for two structs holding the same values, if the padding bytes differ memcmp answers “different”. The members must be compared one by one.
Q. I hear memcmp is dangerous for comparing passwords too?
A. Correct, for a different reason. memcmp returns the instant it meets a differing byte, so the time the comparison took leaks how much of the front matched. That means an attacker can measure time and get it right one byte at a time (a timing attack). When comparing a secret, use a constant-time comparison function that always takes the same time regardless of length — it is not in the standard; cryptographic libraries provide it.
Recap
<string.h> in summary.
| what you want to do | what to use | what not to use |
|---|---|---|
| copy a string | snprintf (or the platform’s strlcpy) | strcpy, a careless strncpy |
| join | snprintf in one go | strcat, strncat with its confusing argument |
| copy that may overlap | memmove | memcpy |
| cut tokens | strcspn/strchr or strtok_r | strtok |
| compare structs | compare member by member | memcmp (padding) |
| compare secrets | constant-time comparison | memcmp (time leak) |
| erase secrets | the platform’s explicit function | memset (vanishes under optimisation) |
Table 66.3
We have passed the strings. The next chapter is the drawer of odds and ends and a treasury of accidents — <stdlib.h>.