strncpy Trap: The "Safe" Function That Forgets the Null Terminator2026-06-10
This function stores a player record from a network packet. The fixed-size field guarantees no overflow, right?
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define USER_LEN 16
typedef struct {
char username[USER_LEN];
int score;
} Player;
Player *make_player(const char *name, int score) {
Player *p = malloc(sizeof(Player));
strncpy(p->username, name, USER_LEN); // "safe" copy
p->score = score;
return p;
}
void print_leaderboard(Player **players, int n) {
for (int i = 0; i < n; i++) {
printf("#%d: %s (%d)\n",
i + 1, players[i]->username, players[i]->score);
}
}
Tests pass: short names print fine, long names don't smash the stack. Ship it. Then production logs start spewing garbage usernames like "alexanderthegre\x2a\x00\x00\x00", and one customer's score appears glued onto their name.
Everyone "knows" strncpy is the safe version of strcpy. Everyone is wrong. Read the spec carefully:
strlen(src) < n: strncpy copies the string and pads the remaining bytes with zeros. (Wasteful, but harmless.)strlen(src) >= n: strncpy copies exactly n bytes and writes no null terminator at all.So when name is "alexanderthegreat" (17 chars), the 16-byte username field is filled but contains no \0. The next printf("%s", ...) happily walks past the array, straight into the score field, then into whatever heap metadata or adjacent allocation follows. On a little-endian machine, a score of 42 (0x2A 00 00 00) gets printed as the suffix "*" plus three nulls — exactly the symptom above. With a non-trivial score, you get arbitrary memory leakage. With no nearby null, you get a crash or a security disclosure.
strncpy was never designed for C strings. It was designed in the 1970s for fixed-width, unterminated directory entries in early Unix filesystems. The null-padding behavior is for that use case, not yours. Using it as a "bounded strcpy" is a category error.
Always force-terminate, or use a function designed for strings:
// Option 1: terminate explicitly
strncpy(p->username, name, USER_LEN - 1);
p->username[USER_LEN - 1] = '\0';
// Option 2: snprintf (truncates and always terminates)
snprintf(p->username, USER_LEN, "%s", name);
// Option 3: strlcpy if available (BSD, glibc 2.38+)
strlcpy(p->username, name, USER_LEN);
snprintf is portable, always null-terminates, and signals truncation through its return value (which is the length it would have written). strlcpy is cleaner but not universal. Either is correct; strncpy for a C string is not.
Related landmines worth knowing: strncat takes "max bytes to append", not buffer size — almost always misused. And strndup does null-terminate, despite the similar name. The strn* family is a museum of inconsistent decisions; check the man page every single time.
strncpy is not "safe strcpy" — when the source meets or exceeds the limit it leaves the destination unterminated, so use snprintf or strlcpy when you actually want a bounded C-string copy.
