C / STRINGS AND BUFFER SAFETY
snprintf instead of sprintf, always
Replace every sprintf call with a bounded snprintf call, read its return value correctly, and detect truncation instead of assuming the output fit.
What you will learn
- Replace sprintf with snprintf and pass the destination's real capacity
- Read n as the wanted length; truncation is n < 0 || (size_t)n >= size
- Clamp before advancing: snprintf's return value can exceed the space you passed
- Size an exact-fit buffer with snprintf(NULL, 0, ...) plus one byte for the NUL
Understanding snprintf instead of sprintf, always
The signature of sprintf carries no capacity, so the number of bytes it writes is decided entirely by the format string and the arguments; the destination array has no say in the matter. Every sprintf call is therefore a silent claim that you computed the worst-case expansion correctly, and that arithmetic is easy to get wrong: %d of INT_MIN is eleven characters, %f of a double near DBL_MAX expands to over three hundred, and %s is bounded only by whatever the caller handed you. snprintf takes the capacity as its second argument, so the bound moves out of your head and into the call, and it will never write to byte number size or beyond.
The return value is the part people get wrong. snprintf returns the length the formatted output would have had, excluding the terminator, not the number of bytes it actually stored. That is deliberate: it lets you discover how much room you needed even in the call that did not have it. The truncation test is therefore n < 0 || (size_t)n >= size, with >= rather than > because size counts the terminating byte, and whenever size is greater than zero the buffer comes back NUL-terminated whether it was truncated or not, so you never plant a terminator by hand.
The pattern that turns this into a bug is p += snprintf(p, remaining, ...) with no check in between. The moment the format wants more than remaining, p jumps past the end of the array and remaining -= n underflows to a value near SIZE_MAX, so the next call is effectively unbounded and you have built something worse than the sprintf you replaced. Compare the return value against the space you had before you move any offset. A related trap is sizeof: it reports the array size only where the array itself is in scope, so inside a function that received char *dst, sizeof dst is the size of a pointer and the capacity has to arrive as its own parameter.
<stdio.h>
<string.h>
int main(void)
{
char buf[16];
const char *host = "database.internal";
int port = 5432;
int n = snprintf(buf, sizeof buf, "%s:%d", host, port);
printf("stored : [%s]\n", buf);
printf("strlen : %zu\n", strlen(buf));
printf("return : %d\n", n);
if (n < 0)
printf("status : output error\n");
else if ((size_t)n >= sizeof buf)
printf("status : truncated, %d chars wanted, %zu bytes available\n",
n, sizeof buf);
else
printf("status : complete\n");
return 0;
}
snprintf bounds the write by the buffer's capacity while reporting the length the output wanted, so truncation is a condition you must test for rather than assume away.
Worked examples
Advancing an offset without walking off the end
Shows the accounting that makes repeated snprintf calls into the same buffer safe, and what a mid-item truncation looks like.
<stdio.h>
/* Keeps *len <= cap - 1 at all times; returns 0 if anything was dropped. */
static int append(char *buf, size_t cap, size_t *len, const char *fmt, int v)
{
size_t room = cap - *len;
int n = snprintf(buf + *len, room, fmt, v);
if (n < 0 || (size_t)n >= room) {
*len = cap - 1; /* snprintf already put the NUL at cap - 1 */
return 0;
}
*len += (size_t)n;
return 1;
}
int main(void)
{
char buf[8];
size_t len = 0;
int all_fit = 1;
int i;
buf[0] = '\0';
for (i = 1; i <= 5; i++)
all_fit = append(buf, sizeof buf, &len, "%d,", i) && all_fit;
printf("buf = [%s]\n", buf);
printf("len = %zu of %zu\n", len, sizeof buf);
printf("all fit = %s\n", all_fit ? "yes" : "no");
return 0;
}
Example explained
Line 1room = cap - *len can never reach zero because the helper caps *len at cap - 1, so snprintf always has at least the one byte it needs for a terminator.
Line 2(size_t)n >= room is the truncation test: n is the length the format wanted, while room includes the byte reserved for the NUL.
Line 3The fourth call gets room == 2, stores '4' and a NUL, and returns 2, so len is clamped to 7 where that NUL already sits and the trailing comma is simply lost.
Line 4The fifth call runs with room == 1, writes only a NUL over the existing one and reports the drop; no byte outside buf[8] is ever touched.
Measuring first, then allocating
Uses a size of zero to ask snprintf how long the result would be, so the buffer can be sized exactly instead of guessed.
<stdio.h>
<stdlib.h>
int main(void)
{
const char *user = "amelia";
int id = 4211;
char *msg;
int need, n;
need = snprintf(NULL, 0, "user=%s id=%d", user, id);
if (need < 0)
return 1;
msg = malloc((size_t)need + 1);
if (msg == NULL)
return 1;
n = snprintf(msg, (size_t)need + 1, "user=%s id=%d", user, id);
printf("need = %d\n", need);
printf("n = %d\n", n);
printf("msg = %s\n", msg);
printf("fits = %s\n", (size_t)n < (size_t)need + 1 ? "yes" : "no");
free(msg);
return 0;
}
Example explained
Line 1A size of 0 forbids any write at all, which is why passing NULL as the destination is legal here and the first call is a pure measurement.
Line 2need counts characters and excludes the terminator, so the allocation has to be need + 1 bytes.
Line 3The second call returns the same 19, and comparing that against the capacity 20 proves the string is complete without calling strlen.
Line 4The arguments are formatted twice, so reserve this idiom for output you genuinely cannot bound rather than using it everywhere.
Important notes
snprintf is standard from C99 onward. Microsoft's older _snprintf is a different function that returns -1 on truncation and leaves the buffer unterminated, so code ported from it needs its checks rewritten.
The destination must not overlap any argument, so snprintf(buf, sizeof buf, "%s.txt", buf) is undefined behaviour even though it reads like a harmless append.
Common mistakes
Writing len += snprintf(buf + len, sizeof buf - len, ...) with no check first: after one truncation len exceeds the buffer, sizeof buf - len underflows to a huge size_t, and the following call writes far outside the array.
Passing sizeof dst inside a function whose parameter is char *dst: the capacity becomes the pointer size, which silently chops every result at seven characters on a 64-bit build and overflows any buffer smaller than eight bytes.
Carrying over the strncpy habit and passing sizeof buf - 1: snprintf's size already covers the terminator, so this quietly drops the last character, and only for inputs that exactly fill the buffer.
Try it yourself
Change, predict, then run
Declare char buf[8] and use snprintf to format "%s/%d" from a name and a number into it, printing buf, strlen(buf) and the return value. Then tune the name until the return value is exactly 7, and again until it is exactly 8, and see which of the two your truncation check flags.
Open the C workspaceCheck your understanding
You call snprintf(buf, sizeof buf, ...) on a char buf[10] and it returns 10. What is true of buf afterwards?
- It holds 9 characters and a NUL, and one character of the intended output was dropped
- It holds the complete output, because the return value matches the buffer size exactly
- It holds 10 characters and a NUL, overflowing the array by one byte
- It was left untouched and unterminated, because a return value equal to the size signals failure
Show answer
The return value counts the characters the format wanted and excludes the terminator, so 10 wanted characters need 11 bytes. The size argument does include the terminator, so only 9 characters fit and the tenth was discarded, with a NUL placed at buf[9]. The second option is tempting because n == size looks like a perfect fit, but the exact-fit boundary is n == size - 1; any n >= size means truncation.