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-boundscatches constant out-of-range indices, but only with optimisation on.at()throwsstd::out_of_range;[]does not check.- Range-based
forremoves 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.