heap error / UB (C++-specific)
3 ------------------------------------------------------------ This file packs THREE classic C++ mistakes, selected by argv[1]: ./16_cpp_pitfalls 1 -> std::vector::operator[] out of bounds ./16_cpp_pitfalls 2 -> std::vector::at() out of bounds (checked) ./16_cpp_pitfalls 3 -> use-after-free with new/delete ./16_cpp_pitfalls 4 -> delete vs delete[] mismatch Build: clang++ -g -O0 16_cpp_pitfalls.cpp -o bin/16_cpp_pitfalls clang++ -g -O0 -fsanitize=address 16_cpp_pitfalls.cpp -o bin/16_cpp_pitfalls.asan ------------------------------------------------------------------ DEMO 1 vs 2 — operator[] (unchecked) vs at() (checked): [lines 106/111] ------------------------------------------------------------------
$ ./bin/16_cpp_pitfalls 1 ; echo "exit=$?" # v[5] on a size-3 vector
.../include/c++/15/bits/stl_vector.h:1263: ... reference
std::vector<int>::operator[](size_type) ... Assertion '__n < this->size()' failed.
Aborted (core dumped)
exit=134$ ./bin/16_cpp_pitfalls 2 ; echo "exit=$?" # v.at(5)
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
Aborted (core dumped)
exit=134
HOW TO READ / THE LESSON:
- .at(5) ALWAYS bounds-checks and throws std::out_of_range — portable,
guaranteed by the standard, with a precise "what()" message. Catch it
or let it terminate; either way it is DEFINED behavior.
- v[5] (operator[]) is normally UNCHECKED: it's plain UB, and on a stock
toolchain it silently reads past the buffer (only ASan would catch it,
as heap-buffer-overflow). HERE it aborts via a libstdc++ assertion
because Fedora builds with _GLIBCXX_ASSERTIONS hardening ON by default
(verify: `clang++ -dM -E -x c++ /dev/null | grep GLIBCXX_ASSERT`).
- TAKEAWAY: prefer .at() when an index might be out of range; rely on
_GLIBCXX_ASSERTIONS / ASan to catch stray operator[] in testing. "It
didn't crash on my machine" depends on how libstdc++ was built.
------------------------------------------------------------------
DEMO 3 — use-after-free with new/delete: [lines 116/117]
------------------------------------------------------------------$ ./bin/16_cpp_pitfalls 3 ; echo "exit=$?"
*p after delete = 183021 # garbage, varies run to run
exit=0 # SILENT on a plain build (no crash)$ ./bin/16_cpp_pitfalls.asan 3
==NNNNN==ERROR: AddressSanitizer: heap-use-after-free on address 0x....
READ of size 4 at 0x.... thread T0
#0 0x... in demo_uaf() .../16_cpp_pitfalls.cpp:117
freed by thread T0 here:
#1 0x... in demo_uaf() .../16_cpp_pitfalls.cpp:116
previously allocated by thread T0 here: ... in demo_uaf() ...:115
HOW TO READ: identical in spirit to the C use-after-free (bug #06), but
with new/delete instead of malloc/free. ASan's quarantine catches the read
at line 117 and points at the delete (line 116). delete p does NOT
null p, so the dangling pointer reads freed memory.
------------------------------------------------------------------
DEMO 4 — delete vs delete[] mismatch: [line 123]
------------------------------------------------------------------$ ./bin/16_cpp_pitfalls 4 ; echo "exit=$?"
deleted (mismatched)
exit=0 # SILENT on a plain build here$ ./bin/16_cpp_pitfalls.asan 4
==NNNNN==ERROR: AddressSanitizer: alloc-dealloc-mismatch
(operator new [] vs operator delete) on 0x....
#0 0x... in operator delete(void*, unsigned long)
#0 0x... in operator new[](unsigned long)
#1 0x... in demo_delete_mismatch() .../16_cpp_pitfalls.cpp:123
HINT: if you don't care about these errors you may set
ASAN_OPTIONS=alloc_dealloc_mismatch=0
HOW TO READ / THE LESSON:
- `new int[8]` must be paired with `delete[]`, never `delete`. The
mismatch is UB: the array cookie/size bookkeeping isn't handled, and
element destructors don't run. Plain run is SILENT here; ASan names it
exactly: "alloc-dealloc-mismatch (operator new [] vs operator delete)".
- RULE: new <-> delete, new[] <-> delete[]. Better: don't write either —
use std::vector / std::unique_ptr and let RAII own the memory.
------------------------------------------------------------------
ROOT CAUSE vs CRASH SITE (all demos):
Demos 1 & 2 fail loudly AT the bad access (assertion/exception) — cause and
site coincide. Demos 3 & 4 are SILENT natively; the "site" only appears
under ASan, and the root cause (dangling pointer / wrong delete form) is a
lifetime/ownership error. The C++ fix is almost always: stop managing raw
memory by hand — use containers and smart pointers (RAII).valgrind also works on C++ (it sees new/delete as malloc/free), but ASan is faster and its C++ messages (alloc-dealloc-mismatch, the demangled "demo_uaf()" frames) are more precise. gdb is best on demos 1/2 (catch the abort: `gdb -ex run -ex bt`); demos 3/4 need a sanitizer.
Rewrite demo_uaf with std::unique_ptr<int> and demo_delete_mismatch with std::vector<int>. Both bugs become impossible — RAII frees exactly once, with the right form, at scope exit. ============================================================
#include <vector>
#include <iostream>
#include <cstdlib>
static void demo_index(void) { /* DEMO 1: operator[] OOB */
std::vector<int> v{10, 20, 30}; /* size 3, valid indices 0..2 */
std::cout << "v[5] = " << v[5] << "\n"; /* <-- BUG: unchecked OOB (UB) */
}
static void demo_at(void) { /* DEMO 2: at() OOB (checked) */
std::vector<int> v{10, 20, 30};
std::cout << "v.at(5) = " << v.at(5) << "\n"; /* <-- throws out_of_range */
}
static void demo_uaf(void) { /* DEMO 3: new/delete use-after-free */
int *p = new int(42);
delete p; /* freed here (p left dangling) */
std::cout << "*p after delete = " << *p << "\n"; /* <-- BUG: use-after-free */
}
static void demo_delete_mismatch(void) { /* DEMO 4: delete vs delete[] */
int *arr = new int[8]; /* array-new */
arr[0] = 1;
delete arr; /* <-- BUG: must be delete[] arr */
std::cout << "deleted (mismatched)\n";
}
int main(int argc, char **argv) {
int which = (argc > 1) ? std::atoi(argv[1]) : 1;
switch (which) {
case 1: demo_index(); break;
case 2: demo_at(); break;
case 3: demo_uaf(); break;
case 4: demo_delete_mismatch(); break;
default: std::cout << "pass 1..4\n";
}
return 0;
}A lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.