Skip to content
VLSI Mentor

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.

ModeWhat happensStatusDetectable?
1 · OverflowMore bytes written than the destination holdsundefined behaviourby a sanitizer
2 · TruncationOnly part of the text is keptperfectly definedonly by you
3 · Missing terminatorA char array that is not a stringundefined when used as oneby 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

Azvya Education Pvt. Ltd.VLSI Mentor
twenty-six bytes into eight
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cc -std=c17 -Wall -Wextra -Wpedantic overflow.c -o overflow
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 26 at ... thread T0
    #0 in main overflow.c:4

WRITE 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.

A five-byte char array followed by three bytes beyond it. The first five cells hold the characters a, b, c, d and e and are marked as the storage the program owns. The next three cells hold f, g and the terminator and are each marked not yours. An arrow shows that the write begins at the first cell and continues past the end of the array.char field[5]all the storage you own[0]'a'[1]'b'[2]'c'[3]'d'[4]'e'[5]'f' · notyours[6]'g' · notyours[7]'\0' · notyoursthe write startshere12
Figure 1 — what an overflow looks like in storage. The buffer is the five bytes on the left, and that is the entirety of what the program owns. A copy of a longer string keeps writing into the bytes after it, which belong to some other object or to nothing at all. Chapter 6.4 established what that means: the standard imposes no requirements, the damage may appear far from here, and the program may well seem to work.

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

Azvya Education Pvt. Ltd.VLSI Mentor
the one nothing will tell you about
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fits:      [sensor-0001]  needed 11, capacity 12, truncated 0
truncated: [temperature]  needed 17, capacity 12, truncated 1
still a valid string? len 11

Read 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 6 at ... thread T0

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

strncpy fixes 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.

Azvya Education Pvt. Ltd.VLSI Mentor
all five, in one function
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  1. Chapter 8.4's nine unverifiable preconditions collapse into three failure modes.
  2. Overflow — writing past the destination. Undefined behaviour, and silent on a normal build.
  3. AddressSanitizer reports it exactly: WRITE of size 26 into an eight-byte buffer.
  4. Truncation — keeping only part of the text. Perfectly defined, and no tool will ever report it.
  5. snprintf returns the length the text would have needed — the only standard truncation detector.
  6. 17 with a capacity of 12 means five characters were dropped, and the result is still a valid string.
  7. Missing terminator — a char array that is not a string; strlen then reads past the end.
  8. A missing terminator turns every later operation into mode one, far from the cause.
  9. strncpy trades mode one for modes two and three; strlcpy and strcpy_s are not ISO C.
  10. Five habits: = {0}, snprintf with its return value checked, carry the capacity, check before strcpy/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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.
  • snprintf returns the length that was needed, which is the only standard way to detect truncation.
  • A missing terminator makes a char array that is not a string, and turns every later operation into an overflow.
  • strncpy is not a safe strcpy — it trades one mode for two others.
  • strlcpy and strcpy_s are not ISO C.
  • The habits: = {0}, snprintf plus 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.