5.1.21 · D2C Programming

Visual walkthrough — Safe alternatives — strncpy, snprintf, strlcpy

2,309 words10 min readBack to topic

Step 1 — What a buffer actually is: a fixed row of boxes

WHAT. In C, char buf[8] is not "a string". It is a wall of exactly 8 numbered boxes, each holding one byte (one character). The boxes are numbered 0, 1, 2, 3, 4, 5, 6, 7 — start at 0, so the last box is number 7, not 8.

WHY. Every safe-vs-unsafe question is really the question "which box did we write into?" If we ever write into box 8, that box does not belong to us — it belongs to whatever variable the compiler parked next. Writing there is the buffer overflow the whole topic is about. So before anything else, we draw the boxes and mark the cliff edge.

PICTURE. Eight boxes. Box 7 is the last legal box (red). Box 8 and beyond is the cliff — not ours.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

Step 2 — What makes a row of boxes a string: the null terminator

WHAT. A row of bytes only becomes a C string when one box holds the special byte '\0' (value zero, the "null terminator"). Reading functions like strlen and printf walk left-to-right and stop the instant they hit '\0'. That box is the end-of-string sign.

WHY do we care about this one byte so much? Because nothing else marks where a string ends. There is no hidden length stored anywhere. If the '\0' is missing, the reader does not stop at box 7 — it keeps walking into box 8, 9, 10... reading garbage until it randomly hits a zero byte somewhere in unrelated memory. So the '\0' is not decoration; it is the brake. (See sizeof vs strlen: sizeof counts boxes, strlen counts characters before the brake.)

PICTURE. The word hi stored in the boxes: 'h' in box 0, 'i' in box 1, and the red '\0' brake in box 2. A reader arrow walks in and screeches to a halt at the red box.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

Step 3 — The unsafe baseline: strcpy never looks at the wall

WHAT. strcpy(dest, src) copies characters from src into dest, box by box, and stops only when it copies the source's own '\0'. It never checks . It does not know, and does not ask, how many boxes dest has.

WHY show the villain first? Because every "safe" function is defined by what it adds to this behaviour. To see the fix, you must first watch the crime.

PICTURE. strcpy(buf, "hello world") — source is 11 characters + a '\0' = 12 bytes — poured into 8 boxes. The pour fills boxes 0–7, then keeps going past the red cliff into boxes 8, 9, 10, 11 — memory we do not own.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

Step 4 — strncpy: stop after n boxes — but watch the brake

WHAT. strncpy(dest, src, n) copies at most n boxes and then stops counting. Two outcomes:

  • If the source (with its '\0') is shorter than n: it copies everything, then fills the leftover boxes with '\0' (zero-padding).
  • If the source is n boxes or longer: it writes exactly n characters and stops — with no '\0' added.

WHY does this matter here? strncpy fixed the overflow (it never writes past box n-1), but in the second outcome it forgets the brake. We stopped writing at the right place, yet we left no end-of-string sign. The row is safe from overflow but is not a valid string.

PICTURE. Two rows, same buffer, same n = 8:

  • Top row: strncpy(buf, "hi", 8)'h' 'i' then six red '\0' pads. Valid string (by luck — source was short).
  • Bottom row: strncpy(buf, "0123456789", 8) → boxes 0–7 hold 0..7, and box 7 is red because it is the last box with no brake anywhere. A reader arrow sails right off box 7 into the cliff.
Figure — Safe alternatives — strncpy, snprintf, strlcpy

Reading the call term-by-term:


Step 5 — snprintf: always reserve the last box for the brake

WHAT. snprintf(dest, size, fmt, ...) formats text (like printf, see Format strings and printf family) but writes at most size bytes total, and that total includes the '\0'. It always stamps the brake as long as size > 0. To guarantee that, it will happily cut the visible text short so the brake still fits in the last box.

WHY is this the behaviour strncpy should have had? strncpy's n counted characters only and could crowd out the brake. snprintf's size counts the whole row including the brake — so the brake is carved out first, and the text gets whatever is left: at most characters. The overflow AND the missing-brake problem are both gone in one stroke.

PICTURE. snprintf(buf, 8, "id=%d", 12345). The full text id=12345 wants 8 characters (needs 9 boxes). Only 8 boxes exist, so snprintf writes id=1234 into boxes 0–6 and puts the red '\0' in box 7. The truncated 5 is shown greyed-out beyond the cliff — it would have been written.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

Step 6 — strlcpy: stop in time, always brake, no wasteful padding

WHAT. strlcpy(dest, src, size) copies up to size - 1 characters, then always stamps the '\0' — and, unlike strncpy, it does not zero-pad the leftover boxes. It returns strlen(src): the length it tried to copy.

WHY does this earn "best behaved"? Line up Steps 4–6:

  • It never overflows (caps at ) — the good half of strncpy.
  • It always brakes — the good half of snprintf.
  • It skips the pointless padding — cheaper than strncpy.

It is strncpy with the trap removed. (Caveat from the parent: non-standard — BSD/macOS, and glibc only since 2.38 — so portable code can't always rely on it.)

PICTURE. strlcpy(buf, "0123456789", 8). Caps at characters → boxes 0–6 hold 0123456, box 7 gets the red brake. Return value (the full source length) floats above, with 10 ≥ 8 flagging the lost data.

Figure — Safe alternatives — strncpy, snprintf, strlcpy


Step 7 — The degenerate cases nobody draws (but you should)

Every function above assumed size > 0 and a sane buffer. Here is what happens at the edges.

Case A — size == 0. There are zero boxes to write. snprintf and strlcpy write nothing at all (they cannot even place a brake — there is no box). snprintf still returns the would-be length; strlcpy still returns strlen(src). Lesson: the "always null-terminates" promise has the fine print "if size > 0". With no boxes there is no brake.

Case B — source exactly fills to . e.g. a 7-character source into an 8-box buffer. strncpy(buf, "1234567", 8) copies 7 chars and — because 7 < 8 — pads box 7 with '\0'. Here strncpy does brake, purely because the source was one char short of trouble. strlcpy/snprintf also brake. All four agree only in this lucky-fit case — which is exactly why the strncpy trap is so easy to miss in casual testing.

Case C — empty source "". All three write a single '\0' into box 0 and stop. Perfectly valid empty string. snprintf returns 0, strlcpy returns 0. No truncation ().

PICTURE. Three mini-rows stacked: (A) size 0 — an empty greyed row, arrow blocked at box 0 in red; (B) exact-fit — 7 chars + a brake that fits; (C) empty source — lone red brake in box 0.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

The one-picture summary

Everything on one wall of boxes. Same buffer char buf[8] (), same oversized source "0123456789", four functions, four rows. Watch only box 7 and the cliff:

  • strcpy — pours past the cliff (red overflow). ✗
  • strncpy — stops at box 7, no brake (red missing-brake). ✗ unless you add it.
  • snprintf — text cut to 7 chars, brake in box 7 (red brake). ✓
  • strlcpy — same safe result, brake in box 7, returns full length. ✓
Figure — Safe alternatives — strncpy, snprintf, strlcpy
Recall Feynman retelling — the whole walkthrough in plain words

A buffer is just a short shelf of numbered boxes; char buf[8] gives us boxes 0 through 7 and nothing more (Step 1). A row of boxes only counts as a "string" when one box holds a stop sign called '\0' — readers walk the shelf and halt at the sign, and if there's no sign they march off the end into strangers' boxes (Step 2). strcpy is reckless: it copies until it hits the source's stop sign, ignoring how many boxes we actually own, so a long source spills off the shelf — that's the overflow (Step 3). strncpy says "I'll copy at most n boxes" — that stops the spill — but if the source is long it uses up all n boxes and forgets to leave a stop sign, so we must slam one into the last box ourselves and copy only so it fits (Step 4). snprintf is smarter: its size counts the whole shelf including the stop sign, so it carves out the last box for the sign first and shortens the text to fit — it always leaves a valid string and tells you, via its return value, how long the text wanted to be so you can spot truncation with r ≥ size (Step 5). strlcpy is the polite one: caps at , always leaves the sign, wastes no time padding, and returns the source's true length (Step 6). At the edges: with size 0 there's not even a box for the sign, so "always terminates" quietly fails; when the source fits exactly with a box to spare, even strncpy looks fine — which is the trap; and an empty source just plants a lone stop sign in box 0 (Step 7). The one number that runs the whole show is : capacity minus the one box reserved for the stop sign.


Connections

  • ← Back to the topic note
  • C strings and the null terminator
  • Buffer overflow and stack smashing
  • strcpy and sprintf (unsafe)
  • sizeof vs strlen
  • Format strings and printf family
  • Memory safety and undefined behavior