Unit 7.6 · about 4 h
47 / 324Undefined behavior, sanitizers and debugger evidence
Undefined behavior has no promised result, including a crash. Tool evidence helps locate a violated contract without turning one observed output into a language rule.
- measure
You can now
- Identify bounds, lifetime, double-free, uninitialized-read and overflow hazards in C++20 code.
- Build with warnings, debug symbols, ASan and UBSan, then interpret a report's failing access and lifetime history.
- Locate a failure in a debugger, repair its root cause and preserve a regression test.
Covers
- Out-of-bounds, lifetime, uninitialized access and undefined behavior
- Compiler diagnostics, sanitizers and debugger evidence
Study, in this order
4 steps
- Undefined behaviorcppreference
Explanation; UB and optimization: Signed overflow, Access out of bounds and Uninitialized scalar
Depth · Use the C++20 rules. Do not mistake displayed assembly for guaranteed behavior; the C++26 erroneous-behavior distinction is outside this unit.
- AddressSanitizerClang AddressSanitizer and UndefinedBehaviorSanitizer
Introduction; Usage and
example_UseAfterFree.cc; Symbolizing the Reports; Additional Checks: Stack Use After Return and Memory leak detection; Limitations - UndefinedBehaviorSanitizerClang AddressSanitizer and UndefinedBehaviorSanitizer
Introduction; Usage; Available checks, including
signed-integer-overflowand theundefinedgroup; Stack traces and report symbolizationDepth · Skip runtime suppression, minimal runtime and newer overflow-behavior attributes.
- Debugger backtracesGDB manual: Backtraces
8.2 Backtraces:
bt,-full, frame numbering and optimized-out argumentsDepth · Skip frame filters and backtrace formatting options.
Focus on
- Bounds and lifetime are separate contracts. A stale pointer may still point inside mapped memory; reading it remains invalid.
- ASan reports memory-access violations with allocation and deallocation stacks. UBSan checks selected undefined operations, including signed overflow; the default
undefinedgroup is not a check for every possible defect. - Preserve the command, compiler version, input and first report. Repair the invalid lifetime or index calculation rather than suppressing the report.
- A debugger gives frames and state, not proof that a program has defined behavior. A clean dynamic-tool run covers only executed paths and enabled checks.
Reproduce closed-book
example_UseAfterFree.cc with no extra arguments, using clang++ 21.1 -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer. Preserve the report: the checked run reports 'AddressSanitizer: heap-use-after-free' and 'READ of size 4', with the allocation, delete[] and subsequent read identified. The unsanitized program has undefined behavior and no particular output is guaranteed. Repair it using initialized owned storage that remains alive until after the read, and rerun with the same flags. In a separate program, add argc to numeric_limits<int>::max(): the same diagnostic build reports "signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'" on this Linux x86-64 toolchain. A default recoverable UBSan report need not produce a nonzero exit; use -fno-sanitize-recover=undefined for a failing test target. On a repaired -O0 -g build, run gdb, break main, run and bt to connect the source location to its frame.Traps
- ASan and UBSan do not generally detect uninitialized-value use. MemorySanitizer is a separate tool, not part of
-fsanitize=address,undefined. - A data race is outside ASan/UBSan's usual checks. ThreadSanitizer requires a separate build and cannot be combined with AddressSanitizer.
- A successful exit is not enough: UBSan can recover after reporting a violation, and an unexecuted bad path remains unchecked.
Practice
AlgoMaster Exercise: Memory Leaks
Exercises 1 to 5: a single heap id, an early-return path, an exception path with unique_ptr, a partially loaded vector and a linked list
Build each unfixed starter with clang++ -fsanitize=address -g and keep the LeakSanitizer report before repairing it; the fixed build must report nothing.
Self-test
-
A use-after-free program prints the expected value. Is the read valid?
No. Its object lifetime ended. Undefined behavior guarantees no particular output, including apparent success. -
What evidence distinguishes an invalid index from a freed-buffer access?
The report's access location and region bounds, plus allocation and deallocation history. Use that history to check the bounds and lifetime independently. -
Why can a UBSan-instrumented test exit zero despite a signed-overflow report?
Many checks recover by default. Configure non-recovery when a violation must fail the test process. -
Does a clean ASan/UBSan run prove there are no uninitialized reads or data races?
No. These tools do not generally cover those defect classes, nor paths that did not execute.
Completion criterion
Stop when
You explain the failing access from its report, preserve a regression that fails on the seeded defect and passes after repair, and name what the enabled tools still cannot prove.
Mark passed, then open 7.7 Only mark this when you can do it without notes.Leads to
- 7.9 Repeatable CMake builds and test targets Keep diagnostic builds and regression tests repeatable.
- 7.8 Python and native code: dtype, stride, ownership and GIL contracts Diagnose native failures without relying on Python exceptions to contain them.
Comes after
- 7.4 Storage duration, RAII, smart pointers and move semanticsUse tool evidence to find lifetime failures.
- 7.5 Templates, containers and iterator invalidationRecognize invalidated-handle bugs before investigating a crash.
Resources for this unit
- cppreference Consult language lifetime, object model, value categories, memory model, atomics, containers and iterator invalidation while reading or modifying code.
- Clang AddressSanitizer and UndefinedBehaviorSanitizer Compile seeded memory defects with AddressSanitizer and UndefinedBehaviorSanitizer, preserve the reports, repair the root cause and keep regression tests.
- GDB manual: Backtraces Use section 8.2 for frame numbers,
bt,bt -fulland optimized-out arguments when inspecting a native failure.
Also in this section
- AlgoMaster C++ Use the sections on compilation, pointers and references, memory, RAII and smart pointers, copy and move, STL containers, templates, errors, concurrency and paired exercises. Skip competitive-programming breadth already covered in Algorithms.
- C++ Core Guidelines Use the Philosophy, Interfaces, Resource management, Expressions, Containers and concurrency rule groups to review ownership and safety decisions in the native component.
- pybind11 documentation Follow First steps and NumPy support to expose one small native function with explicit shape, dtype, stride, ownership, GIL and exception behavior.
- CMake tutorial Use the introductory steps for targets, libraries, testing and install rules. Produce one clean debug build and one optimized build without copied one-off commands.
- Algorithmica - AoS and SoA Read the AoS and SoA experiment for a measured case where keeping each record together wins, the counterpart to a single-field scan.
- `ld.so`: dynamic linker/loader Read DESCRIPTION, Dynamic string tokens and
LD_DEBUGto distinguish link-time symbols from runtime shared-library discovery on Linux. - Memory Layout Transformations Read Overview and Topics through AOS Form and SOA Form for field-wise access and locality trade-offs. Skip Intel array-notation syntax and the LIBOR example.