C++: preventing buffer overflows with bounds checks
Reject writes that do not fit in a buffer and use at() so out-of-range reads fail safely.
#include <cstddef>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
bool copy_name(const std::string& input, char* out, std::size_t capacity) {
if (input.size() + 1 > capacity) {
return false;
}
std::memcpy(out, input.c_str(), input.size() + 1);
return true;
}
int main() {
char buffer[8];
std::cout << copy_name("Ada", buffer, sizeof buffer) << " " << buffer << "\n";
std::cout << copy_name("Programonic", buffer, sizeof buffer) << " " << buffer << "\n";
std::vector<int> scores = {82, 91, 76};
try {
std::cout << scores.at(1) << "\n";
std::cout << scores.at(7) << "\n";
} catch (const std::out_of_range&) {
std::cout << "index 7 rejected\n";
}
}1 Ada 0 Ada 91 index 7 rejected
Explanation
Many serious security flaws are memory-safety bugs. A buffer overflow writes past the end of an array and can overwrite neighbouring data or return addresses. Reading past the end can leak secrets. In C++ the operator[] on arrays and vectors does no checking, so the program must check sizes itself.
The function copies text into a fixed buffer only if the text and its terminating zero fit, and reports failure otherwise. vector::at checks the index and throws std::out_of_range instead of reading invalid memory. Using std::string and std::vector in place of raw arrays removes most of this risk.
Explore the dry run
#include <cstddef>#include <cstring>#include <iostream>#include <stdexcept>#include <string>#include <vector> bool copy_name(const std::string& input, char* out, std::size_t capacity) { if (input.size() + 1 > capacity) { return false; } std::memcpy(out, input.c_str(), input.size() + 1); return true;} int main() { char buffer[8]; std::cout << copy_name("Ada", buffer, sizeof buffer) << " " << buffer << "\n"; std::cout << copy_name("Programonic", buffer, sizeof buffer) << " " << buffer << "\n"; std::vector<int> scores = {82, 91, 76}; try { std::cout << scores.at(1) << "\n"; std::cout << scores.at(7) << "\n"; } catch (const std::out_of_range&) { std::cout << "index 7 rejected\n"; }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Include the terminating zero in the size check. Too long means refuse, never truncate silently into the stack. | — | — | |
| Copy only after the length has been verified. | — | — | |
| The 11-character name needs 12 bytes and the buffer holds 8, so the copy is refused and the buffer still contains Ada. | — | 1 Ada 0 Ada | |
| at(7) throws instead of reading beyond the vector. | std::out_of_range thrown | 1 Ada 0 Ada | |
| The exception is caught and handled. | std::out_of_range thrown | 1 Ada 0 Ada 91 index 7 rejected |
Cost and common mistakes
O(n) for copying n bytes; the checks are O(1).
- Never use strcpy, gets, or sprintf on untrusted input. Use length-checked functions or std::string.
- operator[] on a vector is unchecked, and an out-of-range read is undefined behaviour, not a guaranteed crash.
- Compilers can add protection such as stack canaries and AddressSanitizer, but correct bounds checks come first.
Try it yourself
Why is the terminating zero included in the length check?
Reveal answer and reasoning
A C string needs one extra byte for the terminating '\0'. Without counting it, a name that exactly fills the buffer would write one byte beyond it.