5.1.21 · D5C Programming

Question bank — Safe alternatives — strncpy, snprintf, strlcpy

2,246 words10 min readBack to topic

First, the words this whole page leans on

Before any trap makes sense, three names must be pinned down. The trap questions below use them constantly, so we define them once, plainly, with a picture — never used before this point.

The figure below is the mental model every trap returns to — one row of byte-cells, a red boundary at the buffer's end, and where the '\0' lid should sit.

Figure — Safe alternatives — strncpy, snprintf, strlcpy

And this second figure is the side-by-side buffer walkthrough: the same source "01234567" (exactly 8 characters) poured into an 8-byte buffer by each of the three functions, so you can see who forgets the lid, who truncates, and who pads.

Figure — Safe alternatives — strncpy, snprintf, strlcpy
Recall One-glance summary table
Function Ceiling it uses Text room Always adds '\0'? Return value Negative return?
strncpy n bytes n (no reserve) No dest pointer never
snprintf size (incl. '\0') size - 1 Yes (if size>0) would-be length yes, on encoding error
strlcpy size (incl. '\0') size - 1 Yes (if size>0) strlen(src) never

True or false — justify

strncpy is just a safer strcpy with a length cap and needs no extra care.
False. It caps the byte count at n, but when the source is n bytes or longer (fills all n slots) it writes no '\0', leaving a non-terminated buffer that later reads walk off — you must terminate yourself.
snprintf always leaves you with a valid, null-terminated C string (assuming size > 0).
True. It reserves the final byte for '\0' no matter how long the source is, so the result is always a proper C string — possibly truncated, but always readable.
strlcpy and strncpy produce identical buffer contents for the same arguments.
False. For a short source strncpy zero-pads every remaining byte; strlcpy writes one '\0' and stops. And when the source overflows, strncpy leaves no terminator while strlcpy always terminates.
If snprintf returns a value less than size, the whole string fit without truncation.
Only if the return is non-negative. A return r with 0 <= r < size means everything plus the terminator fit. But a negative return signals an encoding/output error, not "it fit" — so always check r >= 0 first, then r < size.
strlcpy is part of standard C, so you can use it in any portable program.
False. It is a BSD extension, not in the C standard; glibc added it only in version 2.38, so portable code cannot always rely on it being present.
char buf[8]; strncpy(buf, "hi", sizeof(buf)); leaves an unterminated buffer.
False. The source "hi" (2 chars) is shorter than n = 8, so strncpy copies h i \0 then pads the rest with zeros — the buffer is fully terminated here, by luck of the short source.
sizeof(dest) and strlen(dest) give the same number, so either works as the cap.
False. sizeof is the compile-time capacity of the array (constant); strlen is the run-time count of characters before '\0'. For safe capping you want capacity, i.e. sizeof. See sizeof vs strlen.
Truncating a string safely is always fine because "no crash" means "no bug".
False. Silent truncation can corrupt logic — a truncated filename, URL, or command may point somewhere wrong. Safe means no memory corruption; correctness of the truncated value is a separate concern you must still check via the return value.
snprintf can never return a negative number, so r >= size alone is a complete truncation test.
False. On an encoding error (e.g. an invalid multibyte conversion) snprintf returns a negative value. Since size_t/int comparisons can mislead, guard with r < 0 for failure before using r >= size for truncation.

Spot the error

strncpy(dest, src, sizeof(src)); — what's wrong with the size argument?
It caps by the source's size, not the destination's. If src is a pointer, sizeof(src) is the pointer size (often 8), unrelated to how many bytes dest can hold — this can still overflow dest. Always cap by sizeof(dest).
strncpy(dest, src, sizeof(dest)); dest[strlen(dest)] = '\0'; — why is the fix broken?
If strncpy did not terminate, strlen(dest) reads past the buffer to find a '\0', so the index it returns is already garbage. The correct fix indexes the known last slot: dest[sizeof(dest)-1] = '\0';.
if (snprintf(buf, size, ...) > 0) { /* assume no truncation */ } — what's the logic bug?
A positive return only means characters were formatted, not that they fit. Truncation is detected by return >= size (after checking return >= 0), not by "return is positive". A large positive return is exactly the overflow signal.
char buf[8]; snprintf(buf, sizeof(src), "%s", src); — which size is wrong?
The size must be the destination's capacity, sizeof(buf). Passing the source size lets snprintf write up to sizeof(src) bytes into an 8-byte buffer, defeating the whole point. See Buffer overflow and stack smashing.
strncpy(dest, src, sizeof(dest) - 1); with no follow-up line — what did the author forget?
The manual terminator dest[sizeof(dest)-1] = '\0';. strncpy still may not add a '\0' when the source is long enough, so index sizeof(dest)-1 could be non-null and the string is unterminated.
char *p = malloc(8); strlcpy(p, src, 8); then later using p on a Linux box built with old glibc — what breaks?
strlcpy may not exist before glibc 2.38, so the program fails to link or silently uses a different symbol. Portability, not runtime logic, is the trap here.
size_t r = strlcpy(buf, src, 0); — is this safe?
With size == 0 the function writes nothing at all, so it does not null-terminate buf. Any pre-existing garbage in buf remains an unterminated string; guard against a zero size before treating buf as a string. (Remember size is unsigned size_t, so never compute size - 1 without first checking size > 0.)
int r = snprintf(buf, size, ...); if ((size_t)r < size) use(buf); — subtle bug when r is negative?
A negative r cast to size_t becomes a huge unsigned number, so (size_t)r < size is false and the error slips into the "truncated" branch instead of an error branch. Test r < 0 as a signed int first.

Why questions

Why does capping the copy at sizeof(dest) - 1 (not sizeof(dest)) matter?
A string of k visible characters needs k+1 bytes because of the trailing '\0'. Reserving one slot at the end for that terminator is exactly the "−1", so the content can never eat the space the null needs.
Why does snprintf return the would-be length instead of the written length?
So a single number tells you both the result size and whether it fit: compare it to size. If it returned only what fit, you could never tell "exactly full" from "overflowed", losing the truncation signal.
Why must snprintf's return be checked for negativity before comparing to size?
Because output/encoding errors return a negative value; comparing that (especially after an unsigned cast) can be read as "huge, so truncated" and hide a real failure. Negative means failed to format at all, a different problem than truncation.
Why is strncpy's zero-padding considered wasteful?
For a short source into a large buffer it writes '\0' into every remaining byte, which is extra work proportional to the buffer size — pointless when one terminator would do, as strlcpy does.
Why can an unterminated buffer cause a crash far away from the copy itself?
Later code like printf("%s") or strlen keeps reading until it randomly hits a zero byte in adjacent memory. The failure surfaces wherever that read happens — not at the copy — making the bug hard to trace back.
Why is the destination size the "whole game" for these functions?
It is the one piece of information the unsafe functions never had, so they could not know when to stop. Giving them the capacity lets them halt in time; every safety property flows from that single argument.
Why does strlcpy return strlen(src) rather than how many bytes it actually copied?
Reporting the full intended length lets you detect truncation with return >= size, mirroring snprintf. A copied-byte count would always be < size, hiding the fact that data was dropped.

Edge cases

char buf[8]; strncpy(buf, "01234567", 8); — how many characters and is it terminated?
The source is exactly 8 chars = the ceiling n, so all 8 bytes are filled with 0..7 and there is no room and no '\0' — an unterminated 8-byte buffer, the classic trap case (see the middle row of figure s02).
char buf[8]; snprintf(buf, 8, "%s", "01234567"); — same source, what differs from the strncpy case?
snprintf reserves the last byte for the terminator, so buf becomes "0123456\0" (7 chars) and it returns 8. It is truncated but safe and terminated — unlike the strncpy version (bottom row of figure s02).
snprintf(buf, 1, "%s", "hi"); — what ends up in buf?
With size == 1 the single byte is used for '\0', so buf is an empty but valid string "". The return is still 2 (would-be length), flagging truncation.
snprintf(NULL, 0, "id=%d", 12345); — is this a bug, and what is the return?
Not a bug; it is a standard idiom. With size == 0 nothing is written (so NULL is fine) and it returns 8, the length needed — letting you allocate exactly the right buffer before a real call.
snprintf hits an encoding error mid-format — what does it return and what is in buf?
It returns a negative value and the buffer contents are indeterminate/incomplete. This is why failure (r < 0) is a separate branch from truncation (r >= size).
strncpy(dest, src, 0); — what happens?
It copies zero bytes and adds no terminator, so dest is left untouched. If dest held garbage, it is still an unterminated string — a zero ceiling is not a safe no-op.
strncpy when src and dest overlap — is it defined?
No. These functions require non-overlapping memory; overlapping regions are undefined behavior, so results are unpredictable. See Memory safety and undefined behavior.
An empty source strlcpy(buf, "", 8); — what is in buf and the return?
buf[0] is set to '\0' (an empty valid string) and the return is 0, since strlen("") is 0. No truncation, cleanly terminated.
char buf[8]; snprintf(buf, sizeof(buf), "id=%d", 12345); — value of buf and return?
"id=12345" needs 9 bytes but only 8 fit, so buf holds "id=1234" (7 chars + '\0') and the return is 8; since 8 >= 8, truncation is flagged.

Connections