Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 6 of 7

A buffer overrun is reading or writing outside the bounds of an array. C++ does not check, does not warn at run time, and does not stop you.

int values[5] = {1, 2, 3, 4, 5};
values[10] = 99;      // writes 20 bytes past the end. Nothing objects.

That single line is the ancestor of a very large fraction of the security vulnerabilities in the history of computing.

Why the language permits this

It is not an oversight. It is a trade, and the trade is stated in the C++ design principle "you don't pay for what you don't use."

A bounds check on every array access costs a comparison and a branch, every time. In a loop over a million elements that is a million comparisons the compiler often cannot remove. Languages that check — Python, Java, C# — accept that cost. C++ decided the programmer who knows the index is valid should not pay it.

The consequence is that the check is your job, and if you skip it nothing else will do it.

What actually happens

Writing past the end does not hit a wall. It writes into whatever memory happens to follow:

         values[0..4]              next_variable
        +---+---+---+---+---+     +--------+
        | 1 | 2 | 3 | 4 | 5 |     |   77   |
        +---+---+---+---+---+     +--------+
                              ^
                    values[5], values[6], ... land here

So the symptom is not a crash at the bad line. It is a different variable quietly changing value, and a failure somewhere else entirely, possibly much later. That is what makes these bugs so expensive to find: the crime scene and the body are in different places.

If the overrun is large enough to leave the memory your process owns, you get a segmentation fault — which is the lucky outcome, because at least it stops.

The compiler catches some of it, and only with optimisation

int a[5] = {};
int x = a[10];        // constant index, clearly out of range

At -O0 this is silent. With optimisation on:

g++ -std=c++20 -Wall -Wextra -O2 -c main.cpp
warning: array subscript 10 is above array bounds of 'int [5]' [-Warray-bounds=]

Worth knowing precisely: -Warray-bounds needs the optimiser's analysis, so a debug build will not show it. That is one reason to compile with warnings and optimisation in CI even when you develop at -O0.

And it only works when the index is knowable at compile time. The moment the index comes from a file, a user, or a computation, the compiler has nothing to work with.

The safe alternatives

std::vector::at() checks and throws. Deterministically, every time:

#include <iostream>
#include <vector>
#include <stdexcept>

int main() {
    std::vector<int> v = {1, 2, 3};

    std::cout << "v.at(1) = " << v.at(1) << "\n";

    try {
        std::cout << v.at(10) << "\n";
    } catch (const std::out_of_range& e) {
        std::cout << "caught: index was out of range\n";
    }

    std::cout << "still running\n";
    return 0;
}
v.at(1) = 2
caught: index was out of range
still running

v[10] on that same vector would be undefined behaviour with no diagnostic. v.at(10) throws a std::out_of_range you can catch. Use at() wherever the index came from outside your control, and [] in loops you have already proved correct.

Range-based for cannot go out of range, because you never write an index:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {10, 20, 30};

    for (int n : v) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // The container supplies begin and end. There is no index to get wrong.
    std::cout << "size " << v.size() << ", no index written\n";
    return 0;
}
10 20 30 
size 3, no index written

std::string and std::vector know their own size, unlike a C array that decayed to a pointer (lesson 6.1). Most overruns in old code come from a length passed separately from the data and the two drifting apart.

The classic: off-by-one

The most common overrun is exactly one element:

#include <iostream>

int main() {
    int values[5] = {1, 2, 3, 4, 5};
    int sum = 0;

    // Correct: i < 5. Writing i <= 5 reads values[5], one past the end.
    for (int i = 0; i < 5; ++i) {
        sum += values[i];
    }

    std::cout << "sum = " << sum << "\n";
    std::cout << "valid indices are 0 to 4, so the loop must stop at i < 5\n";
    return 0;
}
sum = 15
valid indices are 0 to 4, so the loop must stop at i < 5

<= instead of < reads one int past the array. It will usually appear to work, and usually give a plausible-looking wrong sum.

Finding them

AddressSanitizer catches overruns at run time with the exact line:

g++ -std=c++20 -fsanitize=address -g main.cpp -o main
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd...
WRITE of size 4 at 0x7ffd... thread T0
    #0 0x... in main main.cpp:5

Turn it on for your tests and it finds these before your users do. Module 11 goes into detail.

Try it

Write a program with an int array of 5 and a separate int variable declared right after it. Write to index 5, 6 and 7, then print the separate variable. Then do the same with std::vector::at() and compare.

Checkpoint

  • A buffer overrun reads or writes outside an array; C++ does not check bounds.
  • This is a deliberate trade: no cost for a check you may not need.
  • The symptom is usually a different variable changing, not a crash at the bad line.
  • -Warray-bounds catches constant out-of-range indices, but only with optimisation on.
  • at() throws std::out_of_range; [] does not check.
  • Range-based for removes the index entirely, so it cannot be wrong.
  • AddressSanitizer reports overruns at run time with an exact line.

Questions about this lesson

Log in to ask a question.