C Part I · Module 8 · Strings
String Bugs: Overflow, Truncation and Missing Terminators
The previous chapter ended with a table of nine functions and, for each one, the precondition it is unable to verify. Those nine gaps are not nine separate hazards to memorise — they collapse into three failure modes, and between them those three account for very nearly every string bug ever written in C, including a long line of famous security vulnerabilities. This chapter names all three, shows each one happening with evidence rather than description, and then gives the small set of habits that rules out all of them together. The most useful thing in it is a distinction the usual advice glosses over: two of the three are undefined behaviour that tooling can catch, and the third is perfectly well-defined behaviour that no tool will ever report, because quietly losing half your data is not something a compiler can recognise as wrong.
Intermediate19 min readCPart IFundamentalsStrings
Part I · Module 8 · Chapter 8.5 · String Bugs: Overflow, Truncation and Missing Terminators
1. Three Modes, Not Nine Hazards
Chapter 8.4 §5 listed what each library function cannot check. Reading down that table, two gaps repeat: nothing can check a destination's size, and nothing can check that a terminator exists. Those two gaps produce exactly three ways for string code to go wrong.
| Mode | What happens | Status | Detectable? |
|---|---|---|---|
| 1 · Overflow | More bytes written than the destination holds | undefined behaviour | by a sanitizer |
| 2 · Truncation | Only part of the text is kept | perfectly defined | only by you |
| 3 · Missing terminator | A char array that is not a string | undefined when used as one | by a sanitizer |
The third column is the one to hold on to. Modes 1 and 3 are errors in the sense Chapter 6.4 established — the standard imposes no requirements, and a program doing them is broken whatever it appears to do. Mode 2 is not an error at all. The program is correct C, every operation is defined, and the only thing wrong is that half the label is gone. No compiler and no sanitizer will ever tell you.
2. Mode One: Overflow
#include <stdio.h>
#include <string.h>
int main(void)
{
char small[8] = {0};
const char *source = "this text is far too long";
strcpy(small, source);
printf("%s\n", small);
return 0;
}Built normally:
cc -std=c17 -Wall -Wextra -Wpedantic overflow.c -o overflow(no diagnostics)Nothing. The destination's size is visible in the declaration four lines above, the source is a literal whose length the compiler knows exactly, and there is still no warning — because strcpy is an ordinary function call and Chapter 7.7 established that the size does not survive the call.
Built with AddressSanitizer, the same program reports:
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 26 at ... thread T0
#0 in main overflow.c:4WRITE of size 26 into an eight-byte object. strcpy copied the whole source plus its terminator in one operation, because that is what it was asked to do and it had no way to know better.
This is Chapter 6.4's out-of-range access arriving through a library call, and everything that chapter said applies: it may corrupt a neighbouring object, it may appear to work, and it will not reliably crash. It is also the mechanism behind a long history of security vulnerabilities, for a reason worth stating plainly — the bytes being written past the end come from data, so whoever supplies the data decides what goes there.
3. Mode Two: Truncation
#include <stdio.h>
#include <string.h>
int main(void)
{
char label[12];
int needed = snprintf(label, sizeof label, "%s-%s", "sensor", "0001");
printf("fits: [%s] needed %d, capacity %zu, truncated %d\n",
label, needed, sizeof label, needed >= (int)sizeof label);
needed = snprintf(label, sizeof label, "%s-%s", "temperature", "probe");
printf("truncated: [%s] needed %d, capacity %zu, truncated %d\n",
label, needed, sizeof label, needed >= (int)sizeof label);
printf("still a valid string? len %zu\n", strlen(label));
return 0;
}Output:
fits: [sensor-0001] needed 11, capacity 12, truncated 0
truncated: [temperature] needed 17, capacity 12, truncated 1
still a valid string? len 11Read the second line carefully, because everything about this mode is in it.
Nothing went wrong. No undefined behaviour, no overflow, no missing terminator. label holds "temperature", properly terminated, and strlen confirms it is a perfectly valid eleven-character string.
And the data is gone. The intended text was temperature-probe. What survives is temperature, and the -probe has been silently discarded.
4. Mode Three: Missing Terminator
Chapter 8.1 §5 established that a char array with no zero byte is not a string. Chapter 8.4 §4 showed the commonest way to produce one.
char field[5];
strncpy(field, "abcdefgh", 5); /* writes exactly 5 bytes, no terminator */
printf("%zu\n", strlen(field)); /* walks past the end looking for one */The first line is defined and does exactly what strncpy promises. The second is Chapter 6.4's undefined behaviour, because strlen walks until it finds a zero and there is not one inside the array.
Under AddressSanitizer:
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 6 at ... thread T0A read this time rather than a write, and six bytes into a five-byte object — strlen had already run off the end before anything stopped it.
Three ways this mode arises, all of them seen earlier in the module:
An exactly sized initialiser. char exact[5] = "hello"; — Chapter 8.1 §5, and the one case the compiler does warn about, with -Wunterminated-string-initialization.
A hand-written copy that forgets the final write. Chapter 8.3 §3's destination[i] = '\0'; after the loop.
strncpy with a source at least as long as n. Chapter 8.4 §4, verified.
Note what mode three does to the other two: an unterminated buffer passed to strlen, strcpy, strcat or %s makes every one of them read or write past the end. A missing terminator is therefore not a third independent problem so much as a way of turning every subsequent operation into mode one.
5. Why strncpy Is the Wrong Default
The usual advice is to replace strcpy with strncpy. Put that advice into the taxonomy:
strcpy(dest, source); /* mode 1 risk: overflow */
strncpy(dest, source, sizeof dest); /* mode 1 fixed, mode 3 introduced */The second cannot overflow. It can leave dest unterminated, which §4 just showed turns the next operation into mode one somewhere else in the program, further from the cause.
The correct form needs three lines and a repeated size:
strncpy(dest, source, sizeof dest - 1);
dest[sizeof dest - 1] = '\0';and even then it silently truncates — mode two, undetected, because strncpy returns the destination pointer and tells you nothing.
strncpyfixes mode one by trading it for modes two and three. It was designed for fixed-width padded fields, not as a safe copy, and using it as one requires remembering two extra rules every time.
Some platforms provide strlcpy or strcpy_s, which do terminate and do report. Neither is in ISO C, so neither is portable — Chapter 1.3's distinction, in a place where reaching for a non-standard function is tempting.
6. The Habits
Five habits eliminate all three modes. None is difficult and all of them have appeared earlier in this module.
Initialise every buffer with = {0}. Chapter 8.1 §7 — the buffer is a valid empty string before anything writes to it, so a partially completed operation leaves something printable rather than something undefined.
Build the text with snprintf and check its return value. It cannot overflow, it always terminates, and it is the only standard function that reports truncation. The check is one if.
Keep the size with the buffer. Chapter 7.7 established that the size does not survive a function call, so a function that writes into a caller's buffer needs the capacity as a parameter — the same pointer-plus-length pairing arrays have needed since Module 7.
Check the destination's capacity before strcpy or strcat, when you use them at all. Chapter 8.4 §6 showed the shape: strlen(a) + strlen(b) + 1 <= sizeof dest, written where the array is still an array.
Build and run your tests with -fsanitize=address. It catches modes one and three with their exact byte offsets, as both demonstrations above show. It will not catch mode two, which is why the snprintf check is a separate habit and not an alternative.
#include <stdio.h>
#include <string.h>
#include <stddef.h>
int build_label(char *out, size_t capacity, const char *kind, int index)
{
if (out == NULL || capacity == 0u) {
return -1;
}
int needed = snprintf(out, capacity, "%s-%04d", kind, index);
if (needed < 0 || (size_t)needed >= capacity) {
return -1; /* truncated — the caller decides */
}
return needed;
}
int main(void)
{
char label[16] = {0};
int n = build_label(label, sizeof label, "sensor", 7);
printf("ok n=%2d [%s]\n", n, label);
n = build_label(label, sizeof label, "temperature-probe", 12);
printf("rejected n=%2d [%s]\n", n, label);
return 0;
}Output:
ok n=11 [sensor-0007]
rejected n=-1 [temperature-pro]Four things in that function are worth naming.
The capacity is a parameter, adjacent to the buffer it describes — Chapter 7.7's pairing, and the only way the function can know the size at all.
The pointer is null-checked at the top, which is Chapter 7.4 §5's rule for a pointer arriving from a caller.
Truncation is detected and reported through the return value, so the caller is never left guessing. Note the second line of output: label still contains a valid, terminated string — temperature-pro — because snprintf always terminates. The function returns −1 anyway, because a truncated label is not the label that was asked for.
The caller supplies sizeof label at the call site, where label is still an array. Inside build_label it is a char * and sizeof would give a pointer's size — Chapter 7.7 again.
7. Where These Bugs Actually Come From
Every string in this module has come from a literal or from text the program assembled itself, and the sizes have all been known while writing the code. Real programs get text from outside — a configuration value, a message from a device, a line typed by a person — and that is where mode one becomes a security problem rather than a mistake, because the length is then chosen by whoever supplies the data.
Reading input safely has its own set of traps and its own functions, and it is Chapter 12.3's subject. The habits in §6 are what you bring to it.
8. What You Should Be Able to Say Now
- Chapter 8.4's nine unverifiable preconditions collapse into three failure modes.
- Overflow — writing past the destination. Undefined behaviour, and silent on a normal build.
- AddressSanitizer reports it exactly:
WRITE of size 26into an eight-byte buffer. - Truncation — keeping only part of the text. Perfectly defined, and no tool will ever report it.
snprintfreturns the length the text would have needed — the only standard truncation detector.- 17 with a capacity of 12 means five characters were dropped, and the result is still a valid string.
- Missing terminator — a
chararray that is not a string;strlenthen reads past the end. - A missing terminator turns every later operation into mode one, far from the cause.
strncpytrades mode one for modes two and three;strlcpyandstrcpy_sare not ISO C.- Five habits:
= {0},snprintfwith its return value checked, carry the capacity, check beforestrcpy/strcat, and test under-fsanitize=address.
9. Exercises
Exercise 1 — classify each bug. For each, name the failure mode, say whether it is defined or undefined, and say whether a sanitizer would catch it.
char a[4]; strcpy(a, "hello");
char b[8]; strncpy(b, "abcdefghij", 8); printf("%s\n", b);
char c[6]; snprintf(c, sizeof c, "%s", "abcdefghij");
char d[5] = "hello"; printf("%zu\n", strlen(d));Exercise 2 — predict the snprintf results. For char buf[8], give what is written and what is returned.
snprintf(buf, sizeof buf, "%s", "abc");
snprintf(buf, sizeof buf, "%s", "abcdefg");
snprintf(buf, sizeof buf, "%s", "abcdefgh");
snprintf(buf, sizeof buf, "%d-%d", 100, 200);For each, say whether your truncation check would fire.
Exercise 3 — write the check. Given char out[24] and two const char * pieces, write the condition that must hold before strcpy then strcat would be safe, and say why it must be written where out is an array.
Exercise 4 — repair the copy. This has one failure mode. Name it, give the two-line repair that keeps strncpy, then give the one-line replacement you would actually ship.
char code[6];
strncpy(code, incoming_label, sizeof code);
printf("code is %s\n", code);Exercise 5 — find the silent loss. This compiles, runs, produces no diagnostics under any sanitizer, and is wrong. Say what is wrong, which mode it is, and the smallest change that makes the problem visible.
char path[16];
snprintf(path, sizeof path, "%s/%s", directory, filename);
open_the_thing(path);Exercise 6 — trace the cascade. A buffer is left unterminated by a strncpy. Describe, step by step, what happens when it is later passed to strcat as the destination, and say which mode the final failure is.
Exercise 7 — write the function. Write int append_suffix(char *out, size_t capacity, const char *suffix) that appends to an existing string in out, reports truncation, and never overflows. State its contract in Chapter 5.9's format.
Exercise 8 — audit a build command. Write the compiler command you would use for the test build of a program that handles text, listing every flag from this module and Module 7 that is relevant, with one line each on what it catches. Then say which of the three failure modes is still not covered, and what covers it.
10. Summary
- Nine unverifiable preconditions collapse into three failure modes.
- Overflow is undefined, silent at normal warning levels, and reported precisely by AddressSanitizer.
- Truncation is defined behaviour and undetectable by any tool — only an explicit check finds it.
snprintfreturns the length that was needed, which is the only standard way to detect truncation.- A missing terminator makes a
chararray that is not a string, and turns every later operation into an overflow. strncpyis not a safestrcpy— it trades one mode for two others.strlcpyandstrcpy_sare not ISO C.- The habits:
= {0},snprintfplus its return check, carry the capacity, check before copying, test under a sanitizer. - Text from outside the program is where mode one becomes a security problem — Chapter 12.3.
Module 8 is complete. A string is a char array with a zero byte marking its end; a literal is storage you may read and must not write; length is walked rather than stored; the library's functions all carry preconditions nothing can verify; and three failure modes explain essentially every way string code goes wrong. Every buffer in this module has been an array declared with a size fixed when the code was written — which has quietly constrained every example, because the one thing you cannot do is decide how much storage you need while the program is running. That limitation, and the lifetime questions that come with lifting it, are the subject of the next module.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
