FForge
← Labsdebug-16Runtime debugging●●●○

C++ pitfalls: operator[] vs at(), new/delete UAF, delete[] mismatch

debug/16_cpp_pitfalls.cpp
evidence to leave with
Four demos: assertion, exception, ASan UAF, ASan alloc-dealloc-mismatch
category

heap error / UB (C++-specific)

difficulty

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] ------------------------------------------------------------------

run this
$ ./bin/16_cpp_pitfalls 1 ; echo "exit=$?"     # v[5] on a size-3 vector
what it printed on the machine that wrote this lab
  .../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
run this
$ ./bin/16_cpp_pitfalls 2 ; echo "exit=$?"     # v.at(5)
what it printed on the machine that wrote this lab
  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]
------------------------------------------------------------------
run this
$ ./bin/16_cpp_pitfalls 3 ; echo "exit=$?"
what it printed on the machine that wrote this lab
  *p after delete = 183021          # garbage, varies run to run
  exit=0                            # SILENT on a plain build (no crash)
run this
$ ./bin/16_cpp_pitfalls.asan 3
what it printed on the machine that wrote this lab
  ==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]
------------------------------------------------------------------
run this
$ ./bin/16_cpp_pitfalls 4 ; echo "exit=$?"
what it printed on the machine that wrote this lab
  deleted (mismatched)
  exit=0                            # SILENT on a plain build here
run this
$ ./bin/16_cpp_pitfalls.asan 4
what it printed on the machine that wrote this lab
  ==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).
tool note

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.

take it further

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

read the source · debug/16_cpp_pitfalls.cpp
#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; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.