The Log Line That Isn't Terminated
Your team is adding a diagnostic log to a C11 embedded gateway. A junior developer writes:
void log_tag(const char *tag, const char *msg) {
char line[64];
strncpy(line, tag, sizeof(line));
strncat(line, ": ", sizeof(line) - strlen(line) - 1);
strncat(line, msg, sizeof(line) - strlen(line) - 1);
puts(line);
}
Tags come from a configuration table and are usually 8-20 characters, but a customer-supplied tag can be 80 characters. The first field report shows a crash in strlen during boot, before the first log line is printed. The build uses -O2 and glibc; a colleague says the crash is a stack-smash canary report. The team wants log lines that are always NUL-terminated, never write past line, and truncate rather than abort. The code must remain C11; strlcpy and strlcat are not available.
1. Trace what happens when `tag` is an 80-character string of 'A's. Explain the unterminated buffer, the `strlen` call, and the `size_t` arithmetic.
Read the worked answer
strncpy(line, tag, 64) copies exactly 64 bytes because the source is longer than the specified count. strncpy does not append a terminating NUL when it stops because the count is exhausted; it only pads with NUL if the source is shorter than the count. So line contains 64 'A' bytes and no null terminator anywhere in the array.
The next expression calls strlen(line). strlen requires a pointer to a null-terminated string. Since line is not terminated, strlen reads past the end of the 64-byte array into adjacent stack memory. That is undefined behavior. It may find a zero byte soon, return a value of 64 or more, or run off the stack and fault. The reported crash in strlen and the stack-canary report are consistent with this out-of-bounds read and the subsequent writes.
Even if strlen returns 64, the expression sizeof(line) - strlen(line) - 1 is evaluated in size_t. 64 - 64 - 1 wraps to SIZE_MAX, a huge value. strncat then believes it has an enormous amount of space and writes far past the array. That turns an out-of-bounds read into an out-of-bounds write. The bug is not that 64 is too small; it is that the code uses string functions on a buffer that was never made a string.
2. Rewrite `log_tag` so it always terminates and truncates safely. Explain what `snprintf` guarantees and how to detect truncation if needed.
Read the worked answer
A safe version is:
#include <stdio.h>
void log_tag(const char *tag, const char *msg) {
char line[64];
int n = snprintf(line, sizeof line, "%s: %s", tag, msg);
if (n < 0) {
/* encoding error; optionally log a fixed fallback */
return;
}
/* snprintf already truncated and terminated the result. */
puts(line);
}
snprintf writes at most sizeof line - 1 characters and always appends a NUL terminator when the size argument is greater than zero. It never writes past the array. If the formatted output does not fit, it is truncated, but the result is still a valid string. The return value is the number of characters that would have been written excluding the terminator, or a negative value on encoding error. A caller can detect truncation with if ((size_t)n >= sizeof line), but no extra termination step is required; snprintf already did it.
This removes the unterminated-buffer precondition entirely. Unlike strncpy followed by strncat, there is no intermediate strlen on unvalidated data and no size_t subtraction. If the team later needs to preserve the untruncated message, it can allocate a larger buffer based on the return value, but the fixed stack buffer is safe for logging.
3. Explain the contracts of `strncpy`, `strncat`, and `snprintf` that make the original code unsafe. Why is increasing 64 to 256 not a proper fix?
Read the worked answer
strncpy(dest, src, n) copies at most n bytes. If src is shorter than n, it pads dest with NUL bytes up to n. If src is n or longer, it copies exactly n bytes and does not terminate. The function is therefore not a general bounded string copy. strncat(dest, src, n) requires dest to already be a null-terminated string, then appends at most n characters and always terminates. Its third argument is the maximum number of characters to append, not the size of the destination buffer. The original code computes that argument using strlen on a buffer that may not be terminated, and the subtraction can underflow.
snprintf is different: it is given the total destination size, always terminates when the size is positive, and reports how much output was needed. It can be used without first trusting the contents of the destination. That is the correct contract for converting potentially long input into a fixed buffer.
Increasing 64 to 256 only raises the threshold. An 80-character tag would then fit, but a 300-character tag would reproduce the same unterminated buffer and the same underflow. The function's safety would still depend on input length, which is exactly what the customer-supplied field violates. The fix must make the buffer a valid string regardless of input length, and snprintf does that.