Proven C Book한국어 GitHub

65 Strings and memory — <string.h>

What to know first

chapter 42, Strings · a string is an array that only marks its end
chapter 38, Arrays · handling memory in bulk

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

The danger of strings learned in chapter 42 becomes concrete here, function by function. The twenty-three-chapter gap was needed to erect contracts (chapter 51) and UB (chapter 52) in between — without the word “contract”, “why is strncpy dangerous?” ends at “use it carefully”.

By the end of this chapter

The header in which the most accidents have happened in C. Functions that do not take a size, 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

  1. I hear memcmp is 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.

functionstatusboundarycan truncation be known
strcpy, strcatstandardnone
strncpystandardyesno (must be measured by hand)
strncatstandardyes (but the argument is the remaining room)no
snprintfstandardyesyes (the return value)
strlcpy, strlcatthe BSD family, a C23 annexyesyes (the return value)
strcpy_s, strcat_sC11 annex K (optional)yesyes (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

An old saying. Today the performance difference between the two is mostly negligible, and in some implementations they converge on the same code. The reason 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

functionwhat it doesto beware of
strlenlengthwith no NUL it runs away. 𝑂(𝑛) every time
strcmpcomparison in dictionary orderonly the sign of the return value means anything. 0 is “equal”
strncmpcompare the first n bytesif n exceeds the length it stops at the NUL
strchr, strrchrfind a characterif the sought character is '\0' it points at the end
strstrsubstringworst-case performance differs by implementation
strspn, strcspnlength by a set of charactersthe heart of the cutting idiom
memsetfill with a byte★ for erasing secrets it may vanish under optimisation
memcmpcompare 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 dowhat to usewhat not to use
copy a stringsnprintf (or the platform’s strlcpy)strcpy, a careless strncpy
joinsnprintf in one gostrcat, strncat with its confusing argument
copy that may overlapmemmovememcpy
cut tokensstrcspn/strchr or strtok_rstrtok
compare structscompare member by membermemcmp (padding)
compare secretsconstant-time comparisonmemcmp (time leak)
erase secretsthe platform’s explicit functionmemset (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>.