5.1.21 · D4C Programming

Exercises — Safe alternatives — strncpy, snprintf, strlcpy

2,858 words13 min readBack to topic

Throughout, remember the single mental picture that governs everything: a buffer is a row of numbered boxes. char buf[8] is eight boxes, numbered 0 through 7. A C string is only a valid string when one of those boxes holds the byte '\0' (the "null terminator" — see C strings and the null terminator); that byte is the fence that tells every reader "the text ends here." Overflow means writing into a box that does not exist (box 8 and beyond).

Figure — Safe alternatives — strncpy, snprintf, strlcpy

Level 1 — Recognition

(Can you tell the functions apart by their contract alone?)

Exercise L1.1

Match each function to its behaviour at the boundary (when the source is too long to fit):

  • (a) copies exactly n bytes, leaves no '\0'
  • (b) writes at most size bytes total and always leaves a '\0'
  • (c) writes past the end of the buffer with no check at all

Functions: strcpy, strncpy, snprintf.

Recall Solution
  • strcpy(c). It only stops at the source's own '\0'; it never looks at the destination size. This is the unsafe function from strcpy and sprintf (unsafe).
  • strncpy(a). Capped at n, but on a full/overflowing source it writes exactly n bytes and adds no terminator — the trap.
  • snprintf(b). Total bytes written ≤ size, and it reserves the last box for '\0', so the result is always a valid string when size > 0.

Exercise L1.2

For each function, what does the return value mean? strncpy, snprintf, strlcpy.

Recall Solution
  • strncpy returns dest (the pointer you passed in) — useless for detecting truncation.
  • snprintf returns the number of characters it would have written, excluding the '\0'. Truncation happened iff return >= size.
  • strlcpy returns strlen(src) — the full length it tried to copy. Truncation happened iff return >= size.

The pattern: snprintf and strlcpy both hand you a number you can compare against size. strncpy hands you nothing useful.


Level 2 — Application

(Now compute exact bytes. Draw the boxes.)

Exercise L2.1

char buf[6];
strncpy(buf, "abc", sizeof(buf));

Write out all 6 bytes of buf. Is it a valid C string?

Recall Solution

sizeof(buf) is 6 (six boxes — see sizeof vs strlen for why sizeof gives the array's box count, not the text length). The source "abc" is 3 visible chars plus its own '\0' = 4 bytes, which is shorter than n = 6. strncpy copies a b c \0, then pads the remaining boxes with '\0': Valid string? Yes — box 3 holds a '\0', so strlen(buf) == 3. Safe here purely because the source was short.

Exercise L2.2

char buf[6];
strncpy(buf, "abcdef", sizeof(buf));

Write out all 6 bytes. Is it a valid C string? What is the fix?

Recall Solution

"abcdef" is 6 visible chars. n = 6. strncpy copies exactly 6 bytes of content: No '\0' anywhere — the source filled the entire byte budget, so there was no box left for a fence. Valid? No. strlen(buf) would run off box 5 into adjacent memory (undefined behaviour — see Memory safety and undefined behavior). Fix:

strncpy(buf, "abcdef", sizeof(buf) - 1); // cap at 5
buf[sizeof(buf) - 1] = '\0';             // box 5 = fence

Now buf = a b c d e '\0', giving the truncated string "abcde" (strlen == 5).

Exercise L2.3

char buf[6];
int r = snprintf(buf, sizeof(buf), "val=%d", 42);

What is in buf? What is r? Was there truncation?

Recall Solution

"val=42" is 6 visible chars, needing 7 bytes (6 + the '\0'). Only 6 boxes exist, and snprintf always reserves the last box for '\0'. So it writes 5 content chars + terminator: giving buf = "val=4". r is the length it would have produced = strlen("val=42") = 6. Truncation check: true, truncation happened. (See Format strings and printf family for how %d expands.)


Level 3 — Analysis

(Subtle cases: zero, exact-fit, comparing tools on the same input.)

Exercise L3.1

char buf[8];
size_t r = strlcpy(buf, "0123456789", sizeof(buf));

Give buf and r. Then compare: what would strncpy(buf, "0123456789", sizeof(buf)) leave instead?

Recall Solution

strlcpy caps content at size - 1 = 7 and always terminates: so buf = "0123456". It returns strlen(src) = 10 (the full length it tried). → truncation detected, in one line, for free. Contrast strncpy with the same call: it copies exactly 8 content bytes 0 1 2 3 4 5 6 7 with no terminator — an invalid string. That single difference (fence vs no fence) is the whole reason strlcpy is "the one strncpy should have been."

Exercise L3.2 (exact-fit boundary)

char buf[6];
int r = snprintf(buf, sizeof(buf), "%s", "hello");

"hello" is exactly 5 chars. Does it fit? Give buf, r, and whether the truncation check fires.

Recall Solution

5 content chars + 1 terminator = 6 bytes, which is exactly sizeof(buf) = 6. Perfect fit: buf = "hello". Return r = 5. Truncation check: false. No truncation — correct, because everything (including the fence) fit. This is the important boundary: r == size - 1 is the largest value that means "everything fit."

Exercise L3.3 (degenerate: size 0)

char buf[8];
buf[0] = 'X';
int r = snprintf(buf, 0, "hi");

What happens to buf[0]? What is r?

Recall Solution

When size == 0, snprintf is forbidden from writing any byte at all — not even a '\0'. So buf[0] stays 'X'; the buffer is untouched. But it still computes what it would have written and returns that: strlen("hi") = 2. This is a common idiom — call snprintf(NULL, 0, fmt, ...) first to measure the needed length, then allocate exactly r + 1 bytes and call again. The measure-then-write pattern relies on this rule.


Level 4 — Synthesis

(Write correct code from scratch.)

Exercise L4.1

Write a function copy_safe(char *dst, size_t dstsize, const char *src) using only strncpy** that behaves like strlcpy`: never overflows, always terminates. (Return value not required.)

Recall Solution
void copy_safe(char *dst, size_t dstsize, const char *src) {
    if (dstsize == 0) return;          // no room even for a fence
    strncpy(dst, src, dstsize - 1);    // cap content at C-1
    dst[dstsize - 1] = '\0';           // force the fence into the last box
}

Why each line:

  • The dstsize == 0 guard: with zero boxes we cannot even write a terminator; writing dst[-1] would be an out-of-bounds write (Buffer overflow and stack smashing).
  • strncpy(..., dstsize - 1): reserves the last box, so strncpy can never touch box dstsize - 1.
  • dst[dstsize - 1] = '\0': unconditionally places the fence. If the source was long, this truncates; if short, this box was already '\0' from padding — either way it's correct.

Exercise L4.2

Build the string "user=<name>" into char out[16], and set a flag truncated if the name didn't fit. Use snprintf.

Recall Solution
char out[16];
const char *name = "alexandra";       // 9 chars
int r = snprintf(out, sizeof(out), "user=%s", name);
int truncated = (r >= (int)sizeof(out));

"user=alexandra" is 5 + 9 = 14 chars, needing 15 bytes. sizeof(out) = 16, so it fits with room to spare: truncated = (14 >= 16) = false (0). Correct — no data lost. Note the (int) cast: sizeof yields an unsigned size_t; comparing a signed r against it without the cast can misbehave if r were ever negative (an encoding error). Casting makes the comparison honest.


Level 5 — Mastery

(Combine everything, including portability and measurement idioms.)

Exercise L5.1

You must join two strings a = "hello" and b = "world" into char out[9] as "hello-world" (11 chars). Use the snprintf measure-then-detect idiom. What is r, what is in out, and how many bytes would a correct buffer need?

Recall Solution
char out[9];
int r = snprintf(out, sizeof(out), "%s-%s", "hello", "world");

The full result "hello-world" is 5 + 1 + 5 = 11 chars, needing 12 bytes. Only 9 boxes exist, and snprintf reserves the last for '\0', so it writes 8 content chars + fence: out = "hello-wo". Return r = 11 (the wanted length). Truncation: → true. A correct buffer needs r + 1 = 12 bytes. This is the measure trick: r tells you exactly how big to make it next time.

Exercise L5.2 (portability + full pipeline)

Write portable code that copies src into a heap buffer of exactly the right size, terminated, without ever relying on strlcpy (which may be missing — glibc only shipped it in 2.38). Assume src and malloc succeed.

Recall Solution
size_t need = strlen(src) + 1;   // +1 for the '\0' fence
char *dst = malloc(need);
memcpy(dst, src, need);          // copy content AND the terminator

Why this is bulletproof:

  • strlen(src) counts visible chars (stops at src's own '\0'), so need is exactly content + fence.
  • malloc(need) allocates precisely that many boxes — no overflow possible because the buffer was sized from the data.
  • memcpy(dst, src, need) copies need bytes, and since box need - 1 of src is its '\0', the fence is copied too. No manual termination needed.
  • Uses only standard functions → portable everywhere, no strlcpy dependency. This is the "size the cup to the water" strategy — the inverse of "pour into a fixed cup," and it never truncates.

Exercise L5.3 (interaction quiz)

For char b[8] and source "1234567" (7 chars), fill this table — the exact b contents and return/validity for each function.

Call b bytes Valid string? Return
strncpy(b, "1234567", 8) ? ?
snprintf(b, 8, "%s", "1234567") ? ? ?
strlcpy(b, "1234567", 8) ? ? ?
Recall Solution

Source "1234567" is 7 content chars, needing 8 bytes total (7 + fence). b has exactly 8 boxes — an exact fit.

Call b bytes Valid? Return
strncpy(b, "1234567", 8) 1 2 3 4 5 6 7 \0 Yes b (ptr)
snprintf(b, 8, "%s", "1234567") 1 2 3 4 5 6 7 \0 Yes 7
strlcpy(b, "1234567", 8) 1 2 3 4 5 6 7 \0 Yes 7

The subtle point: here even strncpy produces a valid string! The source "1234567" is 7 chars; strncpy copies 7 content bytes and has 1 byte left, which it fills with the source's own '\0', then pads (nothing left to pad). It succeeds only because the content was n - 1 bytes, not n. Push the source to 8 chars and strncpy breaks while the other two stay safe. snprintf/strlcpy return 7; since , no truncation flagged — correct, everything fit.


Recall Master mnemonic recap

"n forgets the knot; printf and l always tie it." One extra argument — the destination size — is the whole safety story. The size answers "how big is the cup?" Never skip it. And the fence rule: content ≤ capacity − 1, always.


Connections