C / STRINGS AND BUFFER SAFETY
strncpy, strncat and their surprising non-guarantees
Size strncpy and strncat calls correctly, terminate their results yourself, and detect the truncation neither function will ever report.
What you will learn
- Read strncpy as 'write exactly n bytes': no NUL if src is too long, NUL padding if short
- Pass sizeof dest - strlen(dest) - 1 to strncat, never sizeof dest
- Terminate manually: strncpy(d, s, size - 1); then d[size - 1] = '\0';
- Detect truncation yourself by comparing strlen(src) with the limit you passed
Understanding strncpy, strncat and their surprising non-guarantees
strncpy was not written to make strcpy safe. It comes from early Unix code that stored names in fixed-width record fields, and its contract still reflects that: it writes exactly n bytes into the destination, no more and no fewer. If the source is shorter than n it keeps writing NUL bytes until n bytes have been written; if the source is n bytes or longer it stops after n characters and writes no terminator at all. What you get back is a character array of known width, which is a C string only when the source happened to be short enough.
The n in strncat means something completely different: it is the maximum number of bytes taken from the source, not the size of the destination. strncat always appends a terminator, so its worst-case write is strlen(dest) + n + 1 bytes, which means strncat(buf, src, sizeof buf) can run far past the end of buf. The only correct third argument is the room actually left, sizeof buf - strlen(buf) - 1. And because strncat begins by scanning dest for its NUL, dest must already be a valid string; appending to an unterminated strncpy result reads off the end before a single byte is written.
Neither function tells you whether it truncated. Both return their dest argument, a pointer you already held, so a complete copy and a cut-off one are indistinguishable at the call site. Keep the three facts separate in your head: strncpy fills a field of n bytes, strncat caps how much of the source it reads, and noticing truncation is entirely your job, normally by comparing strlen(src) against the limit before you call.
<stdio.h>
<string.h>
static void dump(const char *label, const char *a, size_t n)
{
size_t i;
printf("%s:", label);
for (i = 0; i < n; i++)
printf(" %02X", (unsigned char)a[i]);
putchar('\n');
}
int main(void)
{
char pad[8];
char cut[8];
/* fill with 0x23 first, so the dump shows what strncpy touched */
memset(pad, '#', sizeof pad);
memset(cut, '#', sizeof cut);
strncpy(pad, "abc", sizeof pad); /* src shorter than n */
strncpy(cut, "abcdefghij", sizeof cut); /* src longer than n */
dump("pad", pad, sizeof pad);
dump("cut", cut, sizeof cut);
printf("pad prints as %s, cut has no 00 byte and is not a string\n", pad);
return 0;
}
In strncpy the n counts bytes written into the destination, in strncat it counts bytes read from the source, and neither call tells you it truncated.
Worked examples
strncat's limit is about the source, not the buffer
Shows the only correct way to compute strncat's third argument and how much a naive sizeof would have allowed.
<stdio.h>
<string.h>
int main(void)
{
char buf[16] = "user=";
const char *name = "administrator";
size_t room;
/* strncat caps bytes taken from src and always adds a NUL,
so the space available is size - current length - 1 */
room = sizeof buf - strlen(buf) - 1;
printf("strlen(buf)=%zu room=%zu sizeof buf=%zu\n",
strlen(buf), room, sizeof buf);
strncat(buf, name, room);
printf("buf=[%s] strlen=%zu\n", buf, strlen(buf));
if (strlen(name) > room)
printf("truncated %zu bytes\n", strlen(name) - room);
return 0;
}
Example explained
Line 1Initialising buf as a string matters because strncat first scans dest for its NUL before it appends anything.
Line 2room is 16 - 5 - 1 = 10; passing sizeof buf instead would have permitted 5 + 16 + 1 = 22 bytes of writing into 16.
Line 3strncat copies 10 bytes from name plus one terminator, filling the array exactly to its last byte.
Line 4The return value is just buf, so the truncation report has to compare strlen(name) with room by hand.
Making strncpy produce a real string
Wraps strncpy so the destination is always terminated, and shows that truncation stays silent.
<stdio.h>
<string.h>
/* strncpy, plus the terminator strncpy may not have written */
static void copy_str(char *dst, size_t size, const char *src)
{
if (size == 0)
return;
strncpy(dst, src, size - 1);
dst[size - 1] = '\0';
}
int main(void)
{
char a[6];
char b[6];
copy_str(a, sizeof a, "hi");
copy_str(b, sizeof b, "truncated");
printf("a=[%s] strlen=%zu\n", a, strlen(a));
printf("b=[%s] strlen=%zu\n", b, strlen(b));
return 0;
}
Example explained
Line 1Passing size - 1 to strncpy deliberately leaves the last byte alone, so the store to dst[size - 1] is always in bounds.
Line 2For the short source, strncpy itself zero-fills bytes 2 to 4, which makes the manual terminator redundant but harmless.
Line 3For the long source, strncpy writes five characters and no terminator, so dst[size - 1] = '\0' is the only reason b is a string at all.
Line 4Truncation stays invisible to the caller, so compare strlen(src) with size - 1 whenever a cut-off value must not pass silently.
Important notes
GCC's -Wstringop-truncation fires on strncpy calls that cannot terminate their output; treat it as a report that the truncation is real, not as a complaint about illegal code.
Both functions are undefined if source and destination overlap, and neither can tell you the length the result would have had, which is what snprintf's return value gives you.
Common mistakes
Writing strncat(buf, src, sizeof buf): since the limit counts source bytes, strncat can write strlen(buf) + sizeof buf + 1 bytes and overflow the very buffer the n was meant to protect.
Printing a strncpy destination with %s after copying a source at least n bytes long: there is no terminator, so printf walks past the array and prints neighbouring memory or crashes.
Treating strncpy as a cheap bounded copy: strncpy(buf, "ok", 65536) writes 65534 padding bytes, so a large buffer makes a two-character copy expensive and wipes anything already in it.
Try it yourself
Change, predict, then run
Declare char buf[8], memset it to 'x', call strncpy(buf, "abcdefgh", sizeof buf), and print all eight bytes in hex to confirm no 00 appears. Then change the call so buf becomes a usable seven-character string, and print strlen(buf).
Open the C workspaceCheck your understanding
buf is declared char buf[12] and currently holds the string "log:". Which call can write past the end of buf?
- strncat(buf, msg, sizeof buf - strlen(buf) - 1)
- strncat(buf, msg, sizeof buf)
- strncpy(buf, msg, sizeof buf)
- strncpy(buf, msg, sizeof buf - 1)
Show answer
strncat's third argument limits how much of msg is read, not how much space exists, so with sizeof buf it may write 4 + 12 + 1 = 17 bytes into a 12-byte array. strncpy(buf, msg, sizeof buf) is tempting because it looks equally unbounded, but strncpy writes exactly 12 bytes and stays inside the array; its bug is a missing terminator, not an out-of-bounds write.