C++ Undefined Behavior (UB): Why Release-Only Crashes Happen and How to Catch UB

C++ Undefined Behavior (UB): Why Release-Only Crashes Happen and How to Catch UB

이 글의 핵심

A practical guide to UB: major patterns, why optimized builds behave differently, UndefinedBehaviorSanitizer, and sanitizer combinations for CI.

UB often surfaces as crashes—see segmentation fault debugging.

Introduction: “Debug is fine; Release crashes”

“With optimizations enabled, behavior changes”

Undefined behavior (UB) means the C++ standard imposes no requirements on what happens. Compilers assume UB does not occur and may optimize aggressively.

int arr[5] = {1, 2, 3, 4, 5};
int x = arr[10];  // UB: out-of-bounds read

This article covers:

  • Fifteen common UB patterns (grouped)
  • Why Debug and Release differ
  • UBSan and complementary tools
  • How optimizations interact with UB
  • Short case studies

Table of contents

  1. What is UB?
  2. UB patterns
  3. Debug vs Release
  4. UBSan
  5. Compiler optimization and UB
  6. Case studies
  7. Summary

1. What is undefined behavior?

Definition

If a program has undefined behavior, any outcome is allowed: crash, wrong results, or “impossible” optimizations.

UB vs implementation-defined vs unspecified

TermMeaning
UndefinedNo standard guarantee
Implementation-definedCompiler documents behavior
UnspecifiedOne of several allowed outcomes

2. UB patterns (representative)

  1. Out-of-bounds access
  2. Null pointer dereference
  3. Dangling pointer use
  4. Reading uninitialized automatic variables
  5. Signed integer overflow (int)
  6. Violating strict aliasing rules
  7. Using an object outside its lifetime
  8. Data races
  9. Mismatched allocation/deallocation
  10. Unsequenced conflicting side effects on the same scalar
  11. Invalid pointer arithmetic and dereference
  12. Misaligned access via casts (platform-dependent)
  13. Dangerous virtual calls during construction/destruction (design-dependent)
  14. Modifying string literals
  15. Type punning without memcpy/std::bit_cast where required

3. Debug vs Release

Debug often initializes stack slots or uses patterns that mask bugs; Release may leave variables uninitialized and optimize using “UB cannot happen” reasoning.


4. UBSan

g++ -g -fsanitize=undefined -std=c++17 -o myapp main.cpp
./myapp

Combine with -fsanitize=address and -fsanitize=thread where appropriate (separate builds often).


5. Compiler optimization and UB

Examples: null checks that become unreachable after illegal dereference assumptions; x + 1 > x treated as always true for signed int when overflow is assumed impossible.


6. Case studies (short)

  • Game server: signed underflow in combat math—use saturating or wider types.
  • Image processing: kernel indexing without border checks—clamp or loop interior only.
  • Finance: summing int prices—use long long or checked accumulation.

Summary

UB prevention checklist

  • Initialize before read
  • Validate indices and lifetimes
  • Avoid signed overflow; use wider types or checks
  • Synchronize shared data
  • Match new[]/delete[]
  • No data races

Sanitizer overview

ToolFocus
UBSanMany UB rules
ASanMemory errors
TSanData races

Rules

  1. UB is not “bad luck.”
  2. Debug passing does not prove Release safety.
  3. Use sanitizers in CI.
  4. Treat warnings seriously.
  5. Prefer RAII and safe abstractions.

  • Undefined behavior (longer guide)
  • Segmentation fault
  • Sanitizers
  • Data races

Keywords

undefined behavior, UB, UBSan, release-only crash, signed overflow, data race

Practical tips

  • Enable UBSan during development for representative runs.
  • Test optimized builds regularly—not only Debug.
  • Add sanitizers to CI for main branches.

Closing

Undefined behavior is among the most dangerous C++ issues. Combine warnings, sanitizers, and sound types to ship reliable code.

Next: RAII and smart pointers.


  • Stack overflow
  • Segmentation fault (short)
  • Iterator invalidation
  • Template errors