diff --git a/ABI_Build/Trap18_AllocatorBoundary/README.md b/ABI_Build/Trap18_AllocatorBoundary/README.md new file mode 100644 index 0000000..fbe7b34 --- /dev/null +++ b/ABI_Build/Trap18_AllocatorBoundary/README.md @@ -0,0 +1,53 @@ +# Trap 18 — Allocator Boundary + +**Rule:** memory must be released by the same allocator family, module contract, or exported destroy function that owns it. + +**This trap is not an active undefined-behavior demo.** The repository target uses the correct `malloc`/`free` pairing; sanitizer silence is expected unless a real mismatched release is introduced. + +## The rule + +Allocation is a protocol, not just a pointer value. A pointer returned by `std::malloc` must be released with `std::free`; memory returned by `new` must be released with the matching `delete`; memory owned by a DLL or plugin must be returned through that boundary's documented destroy API unless the contract says the caller owns the buffer. + +Across binary boundaries, "same type" is not enough. Different CRTs, heaps, allocators, and ownership rules can make a release operation invalid even when the address looks ordinary in the debugger. + +## In this code + +`main.cpp` contains one safe target, `Trap18_AllocatorBoundary`, and no guarded unsafe branch. + +| Entity | Role | +|---|---| +| `std::malloc(256)` | allocates from the C heap | +| `FreeDeleter::operator()` | calls `std::free(p)` | +| `std::unique_ptr buffer` | stores the pointer and its matching release policy | + +There is no `Trap18_AllocatorBoundary_unsafe` target because no source file contains `RUN_UNSAFE_EXAMPLE`. The trap demonstrates the correct boundary pattern directly. + +## Why it fails + +The failure category in real code is an allocator-contract violation, often undefined behavior at the deallocation call. The pointer value does not encode which allocator owns it. If the caller guesses the wrong release family or releases memory owned by another module, heap metadata can be corrupted far from the original allocation. + +## Correct direction + +```cpp +struct FreeDeleter { + void operator()(void* p) const { std::free(p); } +}; + +std::unique_ptr buffer(std::malloc(256)); +``` + +For DLL APIs, export `destroy_widget(Widget*)` or require caller-provided storage so ownership never crosses the boundary ambiguously. + +## Detection + +| Tool | Result | +|---|---| +| Heap diagnostics / debug CRT | may report a mismatched or invalid heap release in a real bad case | +| AddressSanitizer | sometimes catches allocator-family mismatches, but not every DLL/CRT contract problem | +| Linker | no — the pointer type does not carry allocator ownership | +| CDB / WinDbg | can prove the safe target releases through `FreeDeleter` to `std::free` | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that follows the owner and deleter path. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 18. diff --git a/ABI_Build/Trap25_ExceptionRAII/README.md b/ABI_Build/Trap25_ExceptionRAII/README.md new file mode 100644 index 0000000..e2fc855 --- /dev/null +++ b/ABI_Build/Trap25_ExceptionRAII/README.md @@ -0,0 +1,55 @@ +# Trap 25 — Exception RAII + +**Rule:** cleanup that must run on every exit path belongs in a destructor, not after the operation that may throw. + +**This trap is not undefined behavior.** A skipped manual cleanup is a resource defect; sanitizer silence is expected because the language unwinding rules are working as designed. + +## The rule + +When an exception is thrown, ordinary statements after the throw are skipped until a matching handler is found. During that unwinding, destructors for fully constructed automatic objects are called in reverse construction order. RAII uses that rule by putting resource release in the destructor of an object whose lifetime is tied to the scope. + +Manual `lock(); throw; unlock();` is wrong because the unlock statement is just another statement that can be bypassed. `std::scoped_lock` is right because unlocking is part of object destruction. + +## In this code + +`main.cpp` creates the safe target, `Trap25_ExceptionRAII`. It creates `std::mutex m`, then enters a `try` block with `std::scoped_lock lock(m)`. It throws `std::runtime_error("failure")`, unwinds the lock, and the `catch(...)` prints `recovered and unlocked`. + +| Entity | Role | +|---|---| +| `std::mutex m` | resource being protected | +| `std::scoped_lock lock(m)` | RAII owner of the lock | +| `throw std::runtime_error("failure")` | non-local exit | +| `catch(...)` | resumes after the lock destructor has run | + +There is no `Trap25_ExceptionRAII_unsafe` target because no source file contains `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The anti-pattern fails when cleanup is manual and appears after a throwing operation. That is a resource leak or stuck-lock defect, not undefined behavior by itself. The real source demonstrates the corrected form directly: the destructor runs during unwinding, so the mutex is unlocked before control reaches the handler. + +## Correct direction + +```cpp +std::mutex m; +try { + std::scoped_lock lock(m); + may_throw(); +} catch (...) { + recover(); +} +``` + +Use RAII wrappers for every resource: locks, files, allocations, handles, transactions, and temporary state changes. + +## Detection + +| Tool | Result | +|---|---| +| Exception breakpoints / debugger stack | yes — shows unwinding enters `std::scoped_lock` destructor | +| Static review | yes — flags cleanup statements placed after throwing work | +| Sanitizers | no — a skipped unlock is not a memory, race, or undefined-behavior report | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that follows construction, throw, destructor, and unlock. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 25. diff --git a/ABI_Build/Trap26_StaticInitOrder/README.md b/ABI_Build/Trap26_StaticInitOrder/README.md new file mode 100644 index 0000000..5de8e12 --- /dev/null +++ b/ABI_Build/Trap26_StaticInitOrder/README.md @@ -0,0 +1,58 @@ +# Trap 26 — Static Initialization Order + +**Rule:** do not make dynamic initialization in one translation unit depend on dynamic initialization in another translation unit. + +**This trap is not plain undefined behavior.** Cross-translation-unit dynamic initialization order is unspecified; sanitizer silence is expected because the defect is a startup ordering contract. + +## The rule + +Objects with static storage duration are initialized before `main`, but the relative order of dynamic initialization across different translation units is unspecified. If one global object's initializer reads another dynamically initialized global from a different translation unit, the program depends on link order or other implementation details. + +The usual fix is construct-on-first-use: put the object behind a function-local static. Since C++11, initialization of a function-local static is ordered by the first call and is thread-safe. + +## In this code + +The trap is split across four files. + +| File | Contribution | +|---|---| +| `state.hpp` | declares `Configuration`, `global_configuration`, `copied_during_static_initialization`, and `safe_configuration()` | +| `config.cpp` | defines `Configuration::Configuration()` as `value(42)`, defines `global_configuration`, and implements `safe_configuration()` with a function-local static | +| `dependent.cpp` | defines `copied_during_static_initialization = global_configuration.value` during static initialization | +| `main.cpp` | safe target prints `safe_configuration().value`; unsafe target prints `copied_during_static_initialization` | + +- **Safe target** (`Trap26_StaticInitOrder`) — uses construct-on-first-use. +- **Unsafe target** (`Trap26_StaticInitOrder_unsafe`, `RUN_UNSAFE_EXAMPLE`) — reads the cross-TU copy made during startup. + +## Why it fails + +The order between `global_configuration` in `config.cpp` and `copied_during_static_initialization` in `dependent.cpp` is unspecified. A build may initialize `global_configuration` first and appear correct, or may evaluate the dependent initializer before the constructor has produced the intended value. Reading a not-yet-constructed object is the defect the pattern invites. + +## Correct direction + +```cpp +const Configuration& safe_configuration() { + static const Configuration value; + return value; +} + +int main() { + use(safe_configuration().value); +} +``` + +Prefer `constinit` when constant initialization is possible; otherwise use function-local statics to make the dependency explicit. + +## Detection + +| Tool | Result | +|---|---| +| Constructor breakpoints / startup trace | yes — shows which dynamic initializer ran first in this build | +| Link-order experiments | useful — can expose the dependency by changing the result | +| Sanitizers | no — unspecified initialization order is not an ASan/UBSan/TSan pattern | +| Compiler/linker | generally no — both translation units are individually well formed | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that observes the startup initializer order. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 26. diff --git a/ABI_Build/Trap27_ODRViolation/README.md b/ABI_Build/Trap27_ODRViolation/README.md new file mode 100644 index 0000000..c8e1a1c --- /dev/null +++ b/ABI_Build/Trap27_ODRViolation/README.md @@ -0,0 +1,55 @@ +# Trap 27 — ODR Violation + +**Rule:** every odr-used class, inline function, and template entity must have the same definition in every translation unit. + +**This trap is IFNDR.** The unsafe program is ill-formed, no diagnostic required, so linker and sanitizer silence is the expected outcome. + +## The rule + +The One Definition Rule is what lets separately compiled translation units agree about a type. If two translation units see different definitions of the same class, they may compile successfully but disagree about size, alignment, layout, calling convention, or generated code. The toolchain is not required to diagnose this category; it is commonly called IFNDR. + +Preprocessor-controlled headers are a common source. A macro that is visible in only one `.cpp` can silently change a type that both sides believe has the same name. + +## In this code + +The trap is split across three files. + +| File | Contribution | +|---|---| +| `packet.hpp` | defines `struct Packet { int id; ... }` and optionally adds `void* payload` when `EXTRA_FIELD` is defined | +| `provider.cpp` | defines `EXTRA_FIELD` only when `RUN_UNSAFE_EXAMPLE` is set, then returns `sizeof(Packet)` from `provider_packet_size()` | +| `main.cpp` | includes `packet.hpp` without `EXTRA_FIELD` and prints its `sizeof(Packet)` beside `provider_packet_size()` | + +- **Safe target** (`Trap27_ODRViolation`) — both translation units see the one-member `Packet`. +- **Unsafe target** (`Trap27_ODRViolation_unsafe`, `RUN_UNSAFE_EXAMPLE`) — `provider.cpp` sees the extra `payload` member, while `main.cpp` does not. + +## Why it fails + +The unsafe target gives the same class name two different definitions. `main.cpp` compiles a four-byte `Packet`; `provider.cpp` compiles a larger `Packet` with a pointer member. This is IFNDR: the program is ill formed even if the linker accepts it and the executable merely prints two different size constants. + +## Correct direction + +```cpp +// packet.hpp +struct Packet { + int id; + void* payload; +}; +static_assert(sizeof(Packet) == expected_packet_size); +``` + +Put the canonical definition in one configuration-controlled header, and make every target consume the same generated configuration. + +## Detection + +| Tool | Result | +|---|---| +| Preprocessed source comparison | yes — shows `packet.hpp` expands differently per translation unit | +| Layout/size assertions in every module | yes — catch disagreement close to the boundary | +| Linker | no — it can link one `provider_packet_size()` symbol while the type definitions disagree | +| Sanitizers | no — IFNDR is not generally diagnosed by ASan/UBSan/TSan | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that compares the consumer and provider size constants. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 27. diff --git a/ABI_Build/Trap28_ABIMismatch/README.md b/ABI_Build/Trap28_ABIMismatch/README.md new file mode 100644 index 0000000..948bb82 --- /dev/null +++ b/ABI_Build/Trap28_ABIMismatch/README.md @@ -0,0 +1,56 @@ +# Trap 28 — ABI Mismatch + +**Rule:** a binary boundary must agree on layout, alignment, packing, calling convention, ownership, and runtime rules, not just field names. + +**This trap is IFNDR-style ABI breakage.** Real ABI mismatches are often ill-formed, no diagnostic required; sanitizer silence is expected because this target is a single-executable simulation. + +## The rule + +An ABI is the compiled contract between separately built code. It includes `sizeof`, `alignof`, member offsets, packing pragmas, architecture, runtime library, exception policy, allocator ownership, and calling convention. Source code that appears to describe the same fields can still produce different binary layouts. + +When producer and consumer decode the same bytes with different offsets or alignment expectations, each side is internally consistent and the boundary is wrong. The compiler and linker are often not in a position to diagnose that contract mismatch. + +## In this code + +The trap is split across three files. + +| File | Contribution | +|---|---| +| `layout.hpp` | defines the reporting struct `Layout` and declares `producer_layout()` / `consumer_layout()` | +| `producer.cpp` | defines packed `ProducerMessage` under `#pragma pack(push, 1)` and reports size, alignment, and `request_id` offset | +| `main.cpp` | defines naturally aligned `ConsumerMessage`, computes `consumer_layout()`, calls `producer_layout()`, and prints both reports | + +There is no `Trap28_ABIMismatch_unsafe` target because no source file contains `RUN_UNSAFE_EXAMPLE`. The safe target is `Trap28_ABIMismatch`; the normal target demonstrates the mismatch directly. + +## Why it fails + +`ProducerMessage` places `request_id` at offset 4 with alignment 1; `ConsumerMessage` places it at offset 8 with natural 8-byte alignment. If bytes produced by one side are consumed as the other layout, the `request_id` field is read from the wrong bytes. In real separate builds this is an ABI contract failure that may be IFNDR or simply outside the C++ type system's ability to diagnose. + +## Correct direction + +```cpp +struct WireMessage { + std::uint32_t version; + std::uint64_t request_id; +}; + +static_assert(offsetof(WireMessage, request_id) == 8); +static_assert(alignof(WireMessage) == 8); +``` + +For stable boundaries, prefer opaque handles or serialized wire formats with explicit versioning and validation. + +## Detection + +| Tool | Result | +|---|---| +| Layout assertions | yes — compare `sizeof`, `alignof`, and every boundary offset | +| ABI/symbol inspection | yes — confirms which module and packing policy produced each side | +| Sanitizers | no — a layout contract mismatch can be entirely in-bounds | +| Linker | no — `producer_layout()` and `consumer_layout()` are valid functions with incompatible assumptions | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that measures the producer and consumer layout facts. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 28. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 24. diff --git a/Concurrency/Trap20_DataRace/README.md b/Concurrency/Trap20_DataRace/README.md new file mode 100644 index 0000000..9437874 --- /dev/null +++ b/Concurrency/Trap20_DataRace/README.md @@ -0,0 +1,51 @@ +# Trap 20 — Data Race + +**Rule:** every shared object that can be accessed by more than one thread must have all conflicting accesses ordered by atomics or synchronization. + +## The rule + +A data race occurs when two threads access the same memory location concurrently, at least one access modifies it, and there is no happens-before relationship between the accesses. In C++, a data race is undefined behavior. The problem is not only a lost update; once the program has a data race, the optimizer may assume the racy execution does not exist. + +`std::atomic` makes the counter operation indivisible. `memory_order_relaxed` is enough for this specific counter because no other payload is being published through the counter; the only invariant is that every increment contributes to one final numeric total. + +## In this code + +`main.cpp` starts two `std::jthread` workers and joins both before printing `counter`. + +| Target | Counter type | Worker operation | +|---|---|---| +| `Trap20_DataRace` | `std::atomic counter{0}` | `counter.fetch_add(1, std::memory_order_relaxed)` | +| `Trap20_DataRace_unsafe` (`RUN_UNSAFE_EXAMPLE`) | `int counter = 0` | `++counter` | + +The safe target has a CTest assertion: `ctest -R Safe_AtomicCounter` expects the final count to contain `200000`. + +## Why it fails + +`++counter` on a plain `int` is a read-modify-write sequence. When both worker threads execute it without synchronization, their reads and writes conflict. That is undefined behavior, even if a particular run merely looks like a smaller final count. + +## Correct direction + +```cpp +std::atomic counter{0}; +auto work = [&] { + for (int i = 0; i < 100000; ++i) + counter.fetch_add(1, std::memory_order_relaxed); +}; +``` + +Use a mutex instead when the operation protects a larger invariant than one atomic integer. + +## Detection + +| Tool | Result | +|---|---| +| ThreadSanitizer | yes — reports the conflicting unsynchronized accesses in the unsafe target | +| MSVC AddressSanitizer | no — MSVC supports ASan, not TSan or MSan, and this is not an address error | +| CDB / WinDbg | shows both threads writing the same address, but breakpoints can serialize the race | +| `ctest -R Safe_AtomicCounter` | asserts the corrected atomic path prints `200000` | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that contrasts atomic writes with plain racy writes. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 20. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 20. diff --git a/Concurrency/Trap21_CheckThenAct/README.md b/Concurrency/Trap21_CheckThenAct/README.md new file mode 100644 index 0000000..8829e90 --- /dev/null +++ b/Concurrency/Trap21_CheckThenAct/README.md @@ -0,0 +1,56 @@ +# Trap 21 — Check Then Act + +**Rule:** the check and the action that relies on it must be protected as one invariant, not as two separate observations. + +**This trap is not automatically undefined behavior.** It is an atomicity and TOCTOU defect; sanitizer silence is expected unless the unprotected interval also permits a concrete data race or lifetime error. + +## The rule + +A successful check describes only the state at the instant of the check. If another thread or process can change that state before the action, the later action is using a stale fact. Locking only around the check is therefore not enough: the lock must cover the check and the use, or the check must produce an owning snapshot that remains valid after the lock is released. + +The same idea applies outside memory. A filesystem `exists()` check followed by a later `open()` is two observations of a pathname, and another process can change the path between them. + +## In this code + +`main.cpp` runs three named variants. + +| Function | Demonstration | Correct form in the safe target | +|---|---|---| +| `check_then_act_lock` | copies `shared` while holding `m` | uses `snapshot = shared` as the lifetime handoff | +| `empty_then_pop` | `empty()` and `back()`/`pop_back()` can be split | one `std::scoped_lock` covers the check and pop | +| `exists_then_open` | `exists(path)` can go stale before reading | opens first, then tests the resulting stream | + +- **Safe target** (`Trap21_CheckThenAct`) — keeps each checked invariant inside one protocol. +- **Unsafe target** (`Trap21_CheckThenAct_unsafe`, `RUN_UNSAFE_EXAMPLE`) — splits the queue check/use and the filesystem check/use. + +## Why it fails + +The defect is the unprotected interval. In `empty_then_pop`, `has_item` is only a stale-capable boolean after the lock is released. In `exists_then_open`, the path may be replaced or removed after `exists()` succeeds. This is a logic/atomicity defect; it becomes undefined behavior only if the interval allows an invalid memory access or data race. + +## Correct direction + +```cpp +std::scoped_lock lock(m); +if (!queue.empty()) { + int value = queue.back(); + queue.pop_back(); + use(value); +} +``` + +For external resources, prefer acquire-and-validate APIs: open the file, then validate the handle you actually acquired. + +## Detection + +| Tool | Result | +|---|---| +| Schedule/timeline review | yes — shows the competing action between check and use | +| ThreadSanitizer | only if the bad window produces an actual data race; it does not prove TOCTOU absence | +| MSVC AddressSanitizer | no — the demonstrated bug is not an address error | +| CDB / WinDbg | useful for explaining the two stops, but breakpoints can hide the interleaving | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that makes the stale interval visible. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 21. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 18 and 19, and practical exercise 6. diff --git a/Concurrency/Trap22_VolatileIsNotSync/README.md b/Concurrency/Trap22_VolatileIsNotSync/README.md new file mode 100644 index 0000000..8228299 --- /dev/null +++ b/Concurrency/Trap22_VolatileIsNotSync/README.md @@ -0,0 +1,52 @@ +# Trap 22 — Volatile Is Not Synchronization + +**Rule:** `volatile` is not a thread-synchronization primitive; use atomics or locks to communicate between threads. + +## The rule + +In C++, `volatile` is about observable accesses to special memory, not inter-thread ordering. It does not make an operation atomic, does not create a happens-before edge, and does not publish ordinary payload writes to another thread. A `volatile bool` flag can still be read and written by different threads without synchronization. + +The usual producer/consumer pattern needs a release operation in the writer and an acquire operation in the reader. The release makes earlier writes visible to a matching acquire; the flag is not merely a polling variable but the synchronization edge. + +## In this code + +`main.cpp` shares `payload` and a readiness flag between a writer `std::jthread` and a reader `std::jthread`. + +| Target | Flag | Payload guarantee | +|---|---|---| +| `Trap22_VolatileIsNotSync` | `std::atomic ready` | `store(..., memory_order_release)` pairs with `load(..., memory_order_acquire)` | +| `Trap22_VolatileIsNotSync_unsafe` (`RUN_UNSAFE_EXAMPLE`) | `volatile bool ready` | no synchronization; `payload` is still racy | + +The unsafe target may appear to print the intended value, but that is only one schedule and one implementation result. + +## Why it fails + +The writer stores `payload = 42` and then writes `ready`; the reader spins on `ready` and then reads `payload`. With `volatile`, those operations are not ordered across threads. The read and write of `payload` are conflicting unsynchronized accesses, so the unsafe target has a data race and therefore undefined behavior. + +## Correct direction + +```cpp +std::atomic ready{false}; +writer: payload = 42; +ready.store(true, std::memory_order_release); + +reader: while (!ready.load(std::memory_order_acquire)) { } +use(payload); +``` + +Use a mutex and condition variable when the payload is more than a simple one-shot publication. + +## Detection + +| Tool | Result | +|---|---| +| ThreadSanitizer | yes — reports the racy `payload` access in the unsafe target | +| MSVC AddressSanitizer | no — MSVC has ASan but not TSan or MSan, and the memory is in bounds | +| CDB / WinDbg | can show the missing acquire/release protocol, but it is not a race detector | +| Compiler warnings | generally none; `volatile` is legal syntax with the wrong contract | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that compares the release/acquire path with the volatile path. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 22. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 21. diff --git a/Concurrency/Trap42_ThreadJoinDetach/README.md b/Concurrency/Trap42_ThreadJoinDetach/README.md new file mode 100644 index 0000000..902f12e --- /dev/null +++ b/Concurrency/Trap42_ThreadJoinDetach/README.md @@ -0,0 +1,56 @@ +# Trap 42 — Thread Join or Detach + +**Rule:** a `std::thread` object must be non-joinable before its destructor runs. + +**This trap is not undefined behavior.** Destroying a joinable `std::thread` is defined to call `std::terminate`, so sanitizer silence is expected; the program aborts by mandate. + +## The rule + +`std::thread` is a handle to an executing thread. If the handle is still joinable at destruction, the standard does not guess whether you meant to join, detach, or abandon shared state. It requires `std::terminate`. + +Joining is also an ownership and lifetime rule. A detached thread must not refer to stack data that can disappear before the thread finishes. `std::jthread` is the safer default for scope-bound work because it joins in its destructor and participates in RAII. + +## In this code + +`main.cpp` runs three named variants. + +| Function | Demonstration | Safe behavior | +|---|---|---| +| `destroyed_while_joinable` | unsafe branch lets `std::thread t{worker, 1}` reach destruction joinable | safe branch prints the skip note and calls `t.join()` | +| `exception_skips_manual_join` | an exception after starting work would skip manual cleanup | `std::jthread guarded{worker, 2}` joins during unwinding | +| `detach_outlives_its_data` | reference capture would be dangerous if detached | joins before `local` dies, then uses `std::vector` with value capture | + +- **Safe target** (`Trap42_ThreadJoinDetach`) — joins or uses `std::jthread`. +- **Unsafe target** (`Trap42_ThreadJoinDetach_unsafe`, `RUN_UNSAFE_EXAMPLE`) — aborts in the first variant. + +The safe target has a CTest assertion: `ctest -R Safe_ThreadJoin` expects `pool size=3`. + +## Why it fails + +This is defined-but-dangerous behavior: `~std::thread` sees `joinable() == true` and calls `std::terminate`. The detached-lifetime case is a separate lifetime hazard: if a detached thread reads a reference after the owning scope ends, that later access can become undefined behavior. + +## Correct direction + +```cpp +std::jthread t{worker, id}; // joins at scope exit + +std::thread manual{worker, id}; +manual.join(); // or detach only with an explicit lifetime contract +``` + +Prefer `std::jthread` for scoped worker ownership. Detach only when the data is owned independently of the launching scope. + +## Detection + +| Tool | Result | +|---|---| +| Runtime / debugger | yes — the unsafe target aborts through `std::terminate` | +| `ctest -R Safe_ThreadJoin` | asserts the RAII pool path reaches `pool size=3` | +| Sanitizers | no — mandated termination is not a memory or race violation | +| Code review | checks every control path for join, detach, or `std::jthread` ownership | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that follows the destructor and unwinding paths. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 42. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 36. diff --git a/Lifetime/Trap02_NonNullNotValid/README.md b/Lifetime/Trap02_NonNullNotValid/README.md new file mode 100644 index 0000000..d68b108 --- /dev/null +++ b/Lifetime/Trap02_NonNullNotValid/README.md @@ -0,0 +1,54 @@ +# Trap 02 — Non-null Is Not Valid + +**Rule:** a stored address is not proof that an object is still alive; liveness must come from an owner or a checked weak observation. + +**This trap is not undefined behavior.** The program uses `std::weak_ptr::lock()` and never dereferences a dead object, so sanitizer silence is expected. + +## The rule + +Pointer-like values and object lifetime are separate facts. A raw pointer, and even the implementation fields inside a `weak_ptr`, can retain a non-null numeric address after the last owning reference has released the object. That address is only historical evidence. + +For `std::shared_ptr`, the strong count controls the owned object's lifetime. A `std::weak_ptr` observes the same control block, but it does not keep the object alive. The only valid way to convert that observation into a usable object is `lock()`, which either creates a new `shared_ptr` or returns empty. + +## In this code + +`main.cpp` creates `owner`, copies it into `observer`, then calls `owner.reset()` before asking the observer for a `snapshot`. + +| Name | Type | Meaning | +|---|---|---| +| `owner` | `std::shared_ptr` | the only strong owner of `42` | +| `observer` | `std::weak_ptr` | a non-owning observation of the control block | +| `snapshot` | `std::shared_ptr` | the result of `observer.lock()` | + +- **Safe target** (`Trap02_NonNullNotValid`) — checks `observer.lock()` and prints the no-live-object branch when the snapshot is empty. +- There is no `_unsafe` target — the source contains no `RUN_UNSAFE_EXAMPLE` branch, and this trap shows the correct rule directly. + +## Why it fails + +The wrong reasoning is defined-but-wrong: treating a non-null stored address as a lifetime proof. After `owner.reset()`, the `int` lifetime has ended even if `observer` still contains implementation pointers that look meaningful in a debugger. + +## Correct direction + +```cpp +std::weak_ptr observer = owner; + +if (auto snapshot = observer.lock()) { + std::cout << *snapshot << '\n'; +} +``` + +Use the snapshot, not the observer's stored address, as the proof. If `lock()` fails, there is no live object to read. + +## Detection + +| Tool | Result | +|---|---| +| Ownership trace / debugger | yes — compare `owner` becoming empty with `observer.lock()` returning an empty `snapshot` | +| AddressSanitizer | no — the safe target performs no invalid access | +| Compiler warnings | none — weak observation and failed lock are valid C++ | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing the observer's address fields are not liveness evidence. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 02. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 1. diff --git a/Lifetime/Trap06_EndedLifetime/README.md b/Lifetime/Trap06_EndedLifetime/README.md new file mode 100644 index 0000000..3c9d0c4 --- /dev/null +++ b/Lifetime/Trap06_EndedLifetime/README.md @@ -0,0 +1,51 @@ +# Trap 06 — Ended Lifetime + +**Rule:** a pointer or iterator must not be used after the lifetime of the object it denotes has ended. + +## The rule + +Object lifetime ends at a specific event: leaving a block for an automatic object, evaluating `delete` for a dynamically allocated object, or invalidating a container element by moving the container's storage. Existing observers are not rewritten when that happens. They may still compare non-null and may still contain the old address. + +Dereferencing such an observer attempts to access an object that no longer exists at that location. That is undefined behavior even when the old bytes are still visible, because the language rule is about the live object, not the numeric address. + +## In this code + +`main.cpp` runs three variants from `main()`: + +| Function | Lifetime-ending event | Safe form | +|---|---|---| +| `scope_exit_dangle` | `local` dies at the closing brace | do not dereference `observer` after the block | +| `deleted_heap_dangle` | `delete observer` ends the dynamic `int` lifetime | set `observer = nullptr` and stop using it | +| `reallocation_dangle` | `values.reserve(values.capacity() + 1)` reallocates the vector buffer | re-read through `values.front()` or reacquire a pointer | + +- **Safe target** (`Trap06_EndedLifetime`) — avoids every stale dereference. +- **Unsafe target** (`Trap06_EndedLifetime_unsafe`, `RUN_UNSAFE_EXAMPLE`) — dereferences `observer` after scope exit, after `delete`, or after vector reallocation. + +## Why it fails + +All three unsafe reads are undefined behavior. The stack address, heap address, or old vector buffer address can still be mapped, but no live `int` object is available through that observer. A plausible printed value is only an accident of one run. + +## Correct direction + +```cpp +std::vector values{1, 2, 3}; +values.reserve(values.capacity() + 1); + +int current = values.front(); // reacquire through the owner +``` + +Keep use inside the owner's lifetime. After any lifetime-ending or invalidating operation, discard old observers and reacquire them from the owner. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | reports the heap/vector use-after-free cases, and may report stack-use-after-scope when supported | +| Debug CRT fill bytes | often shows released heap/vector storage as `0xFEEEFEEE` in Debug builds | +| Compiler warnings | generally none — the stale pointer value is well formed | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session comparing old observers with the current owner state. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 06. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 1. diff --git a/Lifetime/Trap08_StringView/README.md b/Lifetime/Trap08_StringView/README.md new file mode 100644 index 0000000..05c4a8a --- /dev/null +++ b/Lifetime/Trap08_StringView/README.md @@ -0,0 +1,52 @@ +# Trap 08 — `string_view` Does Not Own + +**Rule:** `std::string_view` is only a pointer and a length; it never extends the lifetime of the characters it views. + +## The rule + +A `std::string_view` is a borrowed range. Constructing one from a `std::string` records where the string's characters currently live and how many characters are visible. It does not keep the string alive, prevent mutation, or subscribe to buffer changes. + +The view becomes dangling when the owner dies, when a temporary owner is destroyed at the end of the full expression, or when the owner mutates in a way that changes the character buffer. Reading through a dangling view is undefined behavior. + +## In this code + +`main.cpp` has one helper, `make_owner()`, and three variants called from `main()`: + +| Function | Wrong form | Correct form | +|---|---|---| +| `dangling_return` | `returned_view()` returns a view to local `s` | keep a named `std::string owner` and view that | +| `temporary_binding` | `std::string_view view = make_owner();` | store `make_owner()` in `owner` first | +| `mutated_owner` | use `view` after `owner` is assigned a longer string | set `view = owner` again after mutation | + +- **Safe target** (`Trap08_StringView`) — keeps the owner alive or re-seats the view after mutation. +- **Unsafe target** (`Trap08_StringView_unsafe`, `RUN_UNSAFE_EXAMPLE`) — reads views after the owner has gone away or moved its buffer. + +## Why it fails + +The unsafe target has undefined behavior. The view's pointer and size can still look consistent, but they no longer describe a live character range owned by an active `std::string`. + +## Correct direction + +```cpp +std::string owner = make_owner(); +std::string_view view = owner; + +owner = "replacement text"; +view = owner; // re-seat after mutation +``` + +Return owning strings from factories. Use `string_view` only when the caller can prove the viewed storage outlives every use. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | can report heap-backed dangling reads; small-string and stack cases may be silent | +| Debugger owner/view comparison | yes — compare `view.data()` with the current live owner's `data()` | +| Compiler warnings | incomplete — some tools warn on returning a view to a local, but not on all owner mutations | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session tracking the view pointer against the owning string buffer. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 08. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 10 and 11, and practical exercise 3. diff --git a/Lifetime/Trap09_LambdaCapture/README.md b/Lifetime/Trap09_LambdaCapture/README.md new file mode 100644 index 0000000..ae73fad --- /dev/null +++ b/Lifetime/Trap09_LambdaCapture/README.md @@ -0,0 +1,53 @@ +# Trap 09 — Lambda Capture Lifetime + +**Rule:** a closure stores exactly what the capture list says; references and `this` do not become owned snapshots. + +## The rule + +Lambda capture is a storage decision. Capturing by value copies a value into the closure object. Capturing by reference stores a reference to something outside the closure. Capturing `this` stores the object pointer, not the data members themselves. + +That distinction matters when the closure is returned, stored in `std::function`, or invoked later. The closure can outlive a stack local, an object, or an owner that was present when the lambda was created. Calling it after that referent dies is undefined behavior. + +## In this code + +`main.cpp` calls three variants from `main()`: + +| Function | Unsafe capture | Safe capture | +|---|---|---| +| `escaping_reference_capture` | `bad_by_reference()` returns `[&local]` | `good_by_value()` returns `[local]` | +| `this_capture` | `Session::make_reader()` returns `[this]` | returns `[copy = id]` | +| `owned_member_capture` | stores `Session& ref = *owner` and captures `[&ref]` | captures `[owner]` to share ownership | + +- **Safe target** (`Trap09_LambdaCapture`) — copies the needed integer or captures the `shared_ptr` owner. +- **Unsafe target** (`Trap09_LambdaCapture_unsafe`, `RUN_UNSAFE_EXAMPLE`) — invokes callbacks that kept raw references or pointers after the referent died. + +## Why it fails + +The unsafe callbacks have undefined behavior. The `std::function` object is still alive, but the local `int`, the `Session` object, or the `shared_ptr`-owned object behind the captured reference is not. + +## Correct direction + +```cpp +struct Session { + int id{7}; + std::function make_reader() { + return [copy = id] { return copy; }; + } +}; +``` + +Capture values for deferred work. If the callback needs an object to remain alive, capture an owning handle such as `std::shared_ptr`, not a borrowed reference. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | can report stack-use-after-scope or heap-use-after-free when the bad callback reads dead storage | +| Debugger closure inspection | yes — shows whether the closure stores an integer, raw pointer, or `shared_ptr` pair | +| ThreadSanitizer | no — this is a lifetime bug, not a data race | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session classifying each closure's stored capture. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 09. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 12. diff --git a/Lifetime/Trap23_MovedFrom/README.md b/Lifetime/Trap23_MovedFrom/README.md new file mode 100644 index 0000000..335f585 --- /dev/null +++ b/Lifetime/Trap23_MovedFrom/README.md @@ -0,0 +1,53 @@ +# Trap 23 — Moved-From State + +**Rule:** after a successful move, the source object remains valid, but its value is unspecified unless that type documents a stronger postcondition. + +**The moved-from-state lesson in this trap is not undefined behavior.** Reading the moved-from `std::string` state and the self-moved `std::vector` size relies on unspecified values, so sanitizer silence is expected; the unsafe target also includes a separate null `unique_ptr` dereference that is undefined behavior. + +## The rule + +`std::move` does not move anything by itself. It casts an expression so a move constructor or move assignment may steal resources. For standard library types, the moved-from source remains a valid object: it can be destroyed, assigned to, or used by operations with no precondition on its value. + +Validity is not the same as a known value. A moved-from `std::string` may be empty in one implementation and not in another. `std::unique_ptr` is different because its contract says the source becomes null after a successful move. + +## In this code + +`main.cpp` runs three variants from `main()`: + +| Function | Unsafe assumption | Safe form | +|---|---|---| +| `moved_from_container` | prints `source` as if it still has the old text | calls `source.clear()` before semantic reuse | +| `moved_from_owner` | dereferences moved-from `source` | uses `destination` and checks `source == nullptr` | +| `self_move` | assigns `values = std::move(values)` and relies on its size | avoids self-move | + +- **Safe target** (`Trap23_MovedFrom`) — establishes known state or uses the destination object. +- **Unsafe target** (`Trap23_MovedFrom_unsafe`, `RUN_UNSAFE_EXAMPLE`) — demonstrates unspecified moved-from values and one null owner dereference. + +## Why it fails + +The moved-from `std::string` and self-moved `std::vector` cases are valid-but-unspecified, not undefined behavior. The defect is making a semantic decision from a value the standard does not promise. The moved-from `std::unique_ptr` case is different: dereferencing the guaranteed-null source is undefined behavior. + +## Correct direction + +```cpp +std::string source = "payload"; +std::string destination = std::move(source); + +source.clear(); // known state before reuse +``` + +After a move, either stop using the source for its old meaning, assign it a new value, or call an operation that establishes a documented state. + +## Detection + +| Tool | Result | +|---|---| +| Contract review | required — the key distinction is valid-but-unspecified versus documented-null | +| AddressSanitizer / UndefinedBehaviorSanitizer | no report for unspecified moved-from values; a null dereference may be reported separately | +| Compiler warnings | generally none — moving and then using a valid object is often well formed | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session inspecting the source and destination after each move. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 23. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 16 and 17. diff --git a/Lifetime/Trap24_TemporaryLifetime/README.md b/Lifetime/Trap24_TemporaryLifetime/README.md new file mode 100644 index 0000000..e66b2e0 --- /dev/null +++ b/Lifetime/Trap24_TemporaryLifetime/README.md @@ -0,0 +1,48 @@ +# Trap 24 — Temporary Lifetime + +**Rule:** borrowing a pointer from a temporary object does not extend the temporary's lifetime. + +## The rule + +Temporary objects usually live until the end of the full expression that created them. Binding a temporary directly to a reference can extend that lifetime in specific cases, but extracting a pointer from the temporary is not one of those cases. + +`std::string::c_str()` returns a borrowed pointer into the string's character storage. If the string is a temporary, that storage becomes unavailable when the full expression ends. The pointer value may remain non-null, but it no longer points into a live `std::string`. + +## In this code + +`main.cpp` is deliberately small: + +| Target | Code shape | Lifetime result | +|---|---|---| +| `Trap24_TemporaryLifetime` | `std::string owner = "hello"; std::cout << owner.c_str()` | `owner` stays alive through the print | +| `Trap24_TemporaryLifetime_unsafe` | `const char* p = std::string("hello").c_str();` then prints `p` | the temporary owner dies at the semicolon before the print | + +- **Safe target** (`Trap24_TemporaryLifetime`) — names the owning `std::string`. +- **Unsafe target** (`Trap24_TemporaryLifetime_unsafe`, `RUN_UNSAFE_EXAMPLE`) — stores a `const char*` borrowed from a temporary string. + +## Why it fails + +The unsafe target has undefined behavior. `p` is a borrowed character pointer whose owner was the temporary `std::string`. That owner is destroyed before the next statement begins, so streaming `p` reads through a dangling pointer. + +## Correct direction + +```cpp +std::string owner = "hello"; +const char* p = owner.c_str(); +std::cout << p << '\n'; +``` + +Keep the owner alive for at least as long as the borrowed pointer is used. Prefer passing the `std::string` or `std::string_view` with an explicit owner lifetime when possible. + +## Detection + +| Tool | Result | +|---|---| +| Debugger lifetime trace | yes — line up the full-expression boundary with the later pointer use | +| AddressSanitizer | may catch some dangling reads, but small-string storage and immediate reuse can be silent | +| Compiler warnings | generally none — `c_str()` and pointer assignment are individually valid | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing the borrowed pointer after the temporary string has died. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 24. diff --git a/Lifetime/Trap32_SharedPtrCycle/README.md b/Lifetime/Trap32_SharedPtrCycle/README.md new file mode 100644 index 0000000..4e6f91e --- /dev/null +++ b/Lifetime/Trap32_SharedPtrCycle/README.md @@ -0,0 +1,54 @@ +# Trap 32 — `shared_ptr` Cycle + +**Rule:** `std::shared_ptr` ownership is reference-counted; a cycle of owning edges keeps every object in the cycle alive forever. + +**This trap is not undefined behavior.** The cyclic case is a leak, not an invalid access, so no sanitizer memory error is the expected outcome unless a leak checker is enabled. + +## The rule + +A `shared_ptr` contributes to a control block's strong count. The managed object is destroyed only when that strong count reaches zero. If object `A` owns object `B` and object `B` owns object `A`, dropping the local variables removes only the outside owners; the internal owners keep the counts above zero. + +`std::weak_ptr` is the non-owning companion. It observes a control block without increasing the strong count, and `lock()` is the explicit check that temporarily creates a strong owner only when the object is still alive. + +## In this code + +`main.cpp` calls three variants from `main()`: + +| Function | Ownership shape | Result | +|---|---|---| +| `shared_ptr_cycle_leaks` | `CyclicNode::peer` is a `shared_ptr` in both directions | destructors for the cyclic nodes never run | +| `weak_ptr_breaks_cycle` | `WeakNode::next` owns forward and `WeakNode::prev` observes backward | scope exit destroys both nodes | +| `locking_a_weak_ptr` | `observer` watches one `WeakNode` and calls `lock()` before use | after the owner scope, `observer.expired()` is true | + +- **Safe target** (`Trap32_SharedPtrCycle`) — contains the leaking cycle and the corrected weak-edge forms directly. +- There is no `_unsafe` target — the source contains no `RUN_UNSAFE_EXAMPLE` branch. + +## Why it fails + +The cyclic form is defined-but-wrong. Each node owns the other, so both reference counts remain nonzero after the local `shared_ptr`s are destroyed. No destructor runs for the cycle, and the objects leak. + +## Correct direction + +```cpp +struct WeakNode { + std::shared_ptr next; + std::weak_ptr prev; +}; +``` + +Make at least one edge in every ownership cycle non-owning. Lock a `weak_ptr` only for the short section that needs a live object. + +## Detection + +| Tool | Result | +|---|---| +| `use_count()` / destructor logging | yes — counts stay above zero and cyclic destructors do not run | +| `ctest -R Safe_SharedPtrCycle` | asserts `expired: true` for the weak observer variant | +| AddressSanitizer | no memory error — the objects are still owned by the cycle | +| Leak checker | reports the leaked cyclic nodes when leak detection is enabled | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session following strong counts through the cycle and weak edge. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 32. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 26 and practical exercise 9. diff --git a/Lifetime/Trap35_AutoDropsRef/README.md b/Lifetime/Trap35_AutoDropsRef/README.md new file mode 100644 index 0000000..e8adccd --- /dev/null +++ b/Lifetime/Trap35_AutoDropsRef/README.md @@ -0,0 +1,53 @@ +# Trap 35 — `auto` Drops the Reference + +**Rule:** `auto` deduction strips references and top-level `const`. `auto x = f();` copies, even when `f` returns `T&`. + +**This trap is not undefined behavior.** Every line is well defined; the program is simply wrong. No sanitizer will report it, which is exactly why it survives code review. + +## The rule + +`auto` follows template argument deduction: the reference is removed, top-level `const` and `volatile` are removed, and what remains is the deduced type. So when `f()` returns `Heavy&`, `auto x = f();` deduces `Heavy` and constructs a full copy. Writes to `x` then modify the copy, and the original never changes. + +The same rule drives range-for: `for (auto item : items)` copies every element, mutates the copy, and discards it at the end of the iteration. + +## In this code + +`main.cpp` runs three variants against one `static Heavy` returned by reference. There is no `_unsafe` target — each variant shows the wrong form and its correction side by side. + +| Function | Wrong form | Correct form | +|---|---|---| +| `auto_copies_a_reference` | `auto copy = shared_instance();` | `auto& reference = shared_instance();` | +| `range_for_copies_elements` | `for (auto item : items)` | `for (auto& item : items)` / `for (const auto& item : items)` | +| `keeping_the_exact_type` | — | `decltype(auto) exact = shared_instance();` | + +The output is the assertion: after the copy, `shared hits=0`; after `auto&`, `shared hits=42`. + +## Why it fails + +Nothing is invalid — the copy is a legitimate `Heavy` object with a legitimate lifetime. The bug is that the programmer intended an alias and received a value. The two costs are silent: the mutation goes to the wrong object, and every copy carries the `std::string` payload. + +## Correct direction + +| Intent | Write | +|---|---| +| Observe without copying | `const auto&` | +| Mutate the original | `auto&` | +| Preserve exactly what the expression returned | `decltype(auto)` | +| Deliberately take an independent copy | `auto` — and say so in a comment | + +`decltype(auto)` is the right tool when forwarding a return type you do not control, such as a proxy from `std::vector` (see Trap 34). + +## Detection + +| Tool | Result | +|---|---| +| Debugger type inspection | yes — compare the address of the source and of the deduced variable | +| `ctest -R Safe_AutoReference` | asserts `decltype(auto) shared hits=7` | +| Compiler warnings | generally none; the code is valid | +| ASan / UBSan / TSan | no — there is no memory, lifetime, or race error to find | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that classifies each deduction as copy or alias. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 35. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 29 and practical exercise 10. diff --git a/Memory/Trap01_UseAfterFree/README.md b/Memory/Trap01_UseAfterFree/README.md new file mode 100644 index 0000000..b15b8c4 --- /dev/null +++ b/Memory/Trap01_UseAfterFree/README.md @@ -0,0 +1,54 @@ +# Trap 01 — Use After Free + +**Rule:** a pointer value and an object lifetime are independent facts. Releasing the object does not change the pointers that still hold its address. + +## The rule + +An object's lifetime ends when its storage is deallocated. Every pointer that held its address becomes invalid at that moment, but nothing writes to those pointers — they keep the same numeric value they had before. Accessing an object through such a pointer is undefined behavior, so the compiler is free to assume it never happens, and the program may crash, may print plausible data, or may change behavior between Debug and Release. + +The invalid pointer is a *consequence*. The defect is that a non-owning observer outlived the owner. + +## In this code + +`main.cpp` holds one `Widget` through two names: + +| Name | Type | Owns the object | +|---|---|---| +| `owner` | `std::unique_ptr` | yes | +| `observer` | raw `Widget*` | no | + +- **Safe target** (`Trap01_UseAfterFree`) — reads through `observer` only while `owner` is alive, then calls `owner.reset()` and stops. +- **Unsafe target** (`Trap01_UseAfterFree_unsafe`, `RUN_UNSAFE_EXAMPLE`) — repeats the read *after* `reset()`. + +Note that the safe target's memory is freed at the same point. It is not safe because the memory is in a better state; it is safe because nobody reads it. + +## Why it fails + +`reset()` runs `~Widget` and returns the storage to the allocator. `observer` is not notified and is not cleared. The subsequent read is a heap use-after-free: the storage may have been reused, may hold allocator bookkeeping, or may still contain the old bytes — which is the worst case, because the program then looks correct and the tests pass. + +## Correct direction + +Make ownership explicit instead of tracking it by convention: + +```cpp +auto owner = std::make_shared(); +std::weak_ptr observer = owner; // observation that can be validated +if (auto locked = observer.lock()) { use(*locked); } +``` + +Or keep the raw observer, but bound its use to a scope where the owner is provably alive — and never store it. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | reports it precisely, with the allocation and free stacks | +| Debug CRT fill bytes (`0xFEEEFEEE`) | visible as evidence in a debugger, Debug builds only | +| Release build | typically silent; the read returns whatever was left behind | +| Compiler warnings | none — the code is well formed; the defect is in the lifetime | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session that measures the two pointers diverging. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 01. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 1. diff --git a/Memory/Trap03_UninitializedMemory/README.md b/Memory/Trap03_UninitializedMemory/README.md new file mode 100644 index 0000000..4a9c8c0 --- /dev/null +++ b/Memory/Trap03_UninitializedMemory/README.md @@ -0,0 +1,48 @@ +# Trap 03 — Uninitialized memory + +**Rule:** An object must have a value established before any read observes it. + +## The rule + +Default-initialization does not mean zero-initialization for automatic scalar objects or for objects with scalar members and no user-provided constructor. Until an initialization or assignment establishes a value, reading such an object observes an indeterminate value and has undefined behavior for these `int` examples. + +Value-initialization is different. Braces such as `int count{}`, `new Point{}`, and the omitted members in `Config c{3}` establish zero values for the scalar subobjects. + +## In this code + +`main.cpp` runs three short variants. The safe target is `Trap03_UninitializedMemory`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap03_UninitializedMemory_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `uninitialized_local` | reads `count` after `int count;` | `int count{};` | +| `default_vs_value_new` | reads `p->x` and `p->y` after `new Point` | `new Point{}` | +| `partial_aggregate` | reads every member after `Config c;` | `Config c{3}` value-initializes the rest | + +## Why it fails + +The defect is undefined behavior: the program reads indeterminate `int` values. Debug fill bytes such as `0xCC` or `0xCD` can make the run look repeatable, but those bytes are implementation diagnostics, not C++ values. + +## Correct direction + +```cpp +int count{}; +auto p = std::make_unique(); +Config c{3}; +``` + +Initialize at the declaration point, and prefer brace initialization for aggregates and scalar members. If a value is intentionally unknown, model that state explicitly with `std::optional`. + +## Detection + +| Tool | Result | +|---|---| +| MSVC warning/runtime check | can report simple scalar use before initialization, including `count` | +| MemorySanitizer | yes, on supported Clang platforms; it tracks uninitialized reads | +| AddressSanitizer | no — the storage is allocated and in bounds; ASan is not MemorySanitizer | +| Debugger byte inspection | useful evidence, but fill bytes are Debug-build artifacts | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session comparing initialized values with Debug fill-byte evidence. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 03. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 2. diff --git a/Memory/Trap04_OutOfBounds/README.md b/Memory/Trap04_OutOfBounds/README.md new file mode 100644 index 0000000..8a8da71 --- /dev/null +++ b/Memory/Trap04_OutOfBounds/README.md @@ -0,0 +1,49 @@ +# Trap 04 — Out-of-bounds access + +**Rule:** A one-past address may be formed as a sentinel, but it must not be dereferenced. + +## The rule + +Array and container subscripting is governed by the valid element range, not by whether address arithmetic can produce a numeric pointer. For an array of four elements, indices `0` through `3` name elements; index `4` is one past the end and is only usable for comparison or as an iterator sentinel. + +Passing an array to a function as `const int*` loses the extent. Once the parameter is only a pointer, `sizeof(data)` measures the pointer object, not the original array. + +## In this code + +`main.cpp` runs three variants. The safe target is `Trap04_OutOfBounds`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap04_OutOfBounds_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `unchecked_subscript` | reads `values[index]` with `index == 4` | checks `index < values.size()` | +| `off_by_one_loop` | loops with `i <= values.size()` | loops with `i < values.size()` | +| `decayed_array_size` | computes a count from `sizeof(data)` | refuses to infer the bound; use `std::span` or pass the size | + +## Why it fails + +The first two variants are undefined behavior because they read outside the `std::array` elements. The third variant demonstrates a defined calculation that can become an out-of-bounds bug when the recovered count is trusted. + +## Correct direction + +```cpp +void sum_values(std::span values) { + int sum = 0; + for (int value : values) { sum += value; } +} +``` + +Carry the extent with the data. `std::array`, `std::vector`, and `std::span` make the valid range explicit; `operator[]` still requires you to respect it. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | usually reports the illegal read when the access crosses poisoned bounds | +| MSVC debug STL | reports `std::array` subscript violations in Debug builds | +| Compiler warnings | sometimes catch constant off-by-one cases, but not general runtime indices | +| Release build | often silent; the read may return adjacent storage and appear plausible | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session separating one-past address formation from legal dereference. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 04. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 3 and 4. diff --git a/Memory/Trap05_SignedOverflow/README.md b/Memory/Trap05_SignedOverflow/README.md new file mode 100644 index 0000000..f548516 --- /dev/null +++ b/Memory/Trap05_SignedOverflow/README.md @@ -0,0 +1,51 @@ +# Trap 05 — Signed overflow + +**Rule:** Check signed arithmetic before evaluating it, because overflowing a signed operation is undefined behavior. + +## The rule + +Unsigned arithmetic is defined modulo one more than the maximum representable value. Signed arithmetic is different: if addition, multiplication, or negation cannot represent the mathematical result in the destination type, the behavior is undefined. + +That distinction matters for optimization. After a signed overflow expression has been evaluated, a later check is too late; the compiler may already assume the overflow path never happens. + +## In this code + +`main.cpp` first calls `safe_add(19, 23)`, then demonstrates defined unsigned wrap by incrementing an `unsigned` maximum value. The safe target is `Trap05_SignedOverflow`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap05_SignedOverflow_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `additive_overflow` | `maximum + 1` where `maximum` is `INT_MAX` | reject with `safe_add` before addition | +| `multiplicative_overflow` | `factor * factor` in `int` | cast to `long long` before multiplying | +| `negation_overflow` | `-minimum` where `minimum` is `INT_MIN` | cast to `long long` before negating | + +`ctest -R Safe_SignedOverflow` runs the safe target and checks the root CMake PASS_REGULAR_EXPRESSION for the safe sum and unsigned-wrap line. + +## Why it fails + +The three variant functions are undefined behavior in the unsafe target. The unsigned increment in `main` is not UB; it is defined modulo arithmetic and is included to make the contrast explicit. + +## Correct direction + +```cpp +if (auto sum = safe_add(a, b)) { + use(*sum); +} +const long long product = static_cast(factor) * factor; +``` + +Validate before the operation or move the operation into a type that can represent the result. Casting after an overflowing `int` multiplication would still be too late. + +## Detection + +| Tool | Result | +|---|---| +| Clang/GCC UBSan | reports signed integer overflow at runtime | +| Compiler warnings | catch some constant or obviously bounded cases | +| MSVC AddressSanitizer | no — this is not a memory-addressing error | +| Debugger inspection | shows operands and observed machine result, but does not make UB defined | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session classifying addition, multiplication, negation, and unsigned wrap. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 05. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 5 and practical exercise 1. \ No newline at end of file diff --git a/Memory/Trap07_IteratorInvalidation/README.md b/Memory/Trap07_IteratorInvalidation/README.md new file mode 100644 index 0000000..e4eb9b8 --- /dev/null +++ b/Memory/Trap07_IteratorInvalidation/README.md @@ -0,0 +1,49 @@ +# Trap 07 — Iterator invalidation + +**Rule:** After a container operation that invalidates an iterator, pointer, or reference, reacquire it before use. + +## The rule + +Iterator validity is a container contract. A stored address may still contain bytes after a mutation, but the program no longer has permission to use it if the operation invalidated that handle. + +The invalidation rule depends on both the container and the operation. `std::vector` reallocation invalidates all iterators, pointers, and references into the old buffer. `std::vector::erase` invalidates the erased position and following positions. `std::map::erase` destroys only the erased node; other iterators remain valid. + +## In this code + +`main.cpp` runs three short variants. The safe target is `Trap07_IteratorInvalidation`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap07_IteratorInvalidation_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `reallocation_invalidation` | dereferences `old` and `old_reference` after `v.push_back(4)` may reallocate | uses `v.begin()` and `v.front()` after the mutation | +| `erase_invalidation` | dereferences `it` after `v.erase(it)` | continues with the returned `next` iterator | +| `node_invalidation` | dereferences `erased` after `m.erase(erased)` | uses `survivor`, which still refers to a live map node | + +## Why it fails + +The unsafe dereferences are undefined behavior. The object or node may have moved, been destroyed, or simply no longer be reachable through that handle under the standard container contract. + +## Correct direction + +```cpp +auto next = v.erase(it); +for (auto current = next; current != v.end(); ++current) { + use(*current); +} +``` + +Use returned iterators, reacquire references after mutation, and reserve capacity only when that is genuinely part of the invariant you need. + +## Detection + +| Tool | Result | +|---|---| +| Debug iterator checks | often report invalidated iterator use in Debug STL builds | +| AddressSanitizer | catches stale storage reads when invalidation also reaches poisoned storage | +| Compiler warnings | generally no; the operations are well formed and validity is path-dependent | +| Release build | often silent; stale storage may still hold the old-looking value | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session comparing vector buffers, erased iterators, and surviving map nodes. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 07. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 8 and 9 and practical exercise 2. diff --git a/Memory/Trap16_MemsetObject/README.md b/Memory/Trap16_MemsetObject/README.md new file mode 100644 index 0000000..bc54f55 --- /dev/null +++ b/Memory/Trap16_MemsetObject/README.md @@ -0,0 +1,46 @@ +# Trap 16 — `memset` object + +**Rule:** Byte writes are not class operations; use the type's constructor, assignment, or member functions to change object state. + +## The rule + +A live class object has invariants that are maintained by its constructors, destructors, assignment operators, and member functions. Writing raw zero bytes across the complete object representation bypasses those operations and overwrites private representation fields the program is not allowed to manage by convention. + +For trivially copyable byte-oriented data this can be intentional. For `std::string`, zeroing the object is not the same operation as assigning an empty string or calling `clear()`. + +## In this code + +`main.cpp` creates `std::string text = "owned characters"`, then either clears it correctly or overwrites its representation. The safe target is `Trap16_MemsetObject`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap16_MemsetObject_unsafe`. + +| Target | Operation | Meaning | +|---|---|---| +| `Trap16_MemsetObject` | `text.clear()` | type-aware operation preserves the string invariant | +| `Trap16_MemsetObject_unsafe` | `std::memset(&text, 0, sizeof text)` | raw byte write overwrites every implementation field | + +## Why it fails + +The unsafe branch has undefined behavior because it corrupts the representation of a live `std::string` object outside the class contract. The program may still print a size that looks reasonable, but a plausible observation is not a valid invariant. + +## Correct direction + +```cpp +std::string text = "owned characters"; +text.clear(); +text = {}; +``` + +Let the class perform the state transition. Reserve `std::memset` for raw storage or trivial byte buffers where the representation operation is the actual intent. + +## Detection + +| Tool | Result | +|---|---| +| Debugger byte inspection | shows that `memset` changed fields `clear()` preserved | +| AddressSanitizer | usually no; the write is in bounds and the object storage is alive | +| Compiler warnings | usually no; `std::memset` accepts `void*` and the call is syntactically valid | +| Runtime output | not reliable; the object can look empty while its invariant is broken | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session comparing `std::string` representation before and after `clear()` and `memset`. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 16. diff --git a/Memory/Trap17_NewDeleteMismatch/README.md b/Memory/Trap17_NewDeleteMismatch/README.md new file mode 100644 index 0000000..b8132b5 --- /dev/null +++ b/Memory/Trap17_NewDeleteMismatch/README.md @@ -0,0 +1,49 @@ +# Trap 17 — New/delete mismatch + +**Rule:** The deallocation and destruction protocol must match the way storage and object lifetime were created. + +## The rule + +C++ has several different allocation and lifetime protocols. `new[]` must be matched with `delete[]`; scalar `new` must be matched with scalar `delete`; `malloc` storage must be released with `free`; placement `new` constructs an object in caller-owned storage and does not allocate anything to delete. + +The pointer value alone does not record enough information to repair a mismatch later. The program must preserve the correct ownership protocol from the creation site. + +## In this code + +`main.cpp` runs three short variants around `Widget`. The safe target is `Trap17_NewDeleteMismatch`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap17_NewDeleteMismatch_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `array_form_mismatch` | `new Widget[3]` followed by scalar `delete` | `std::make_unique(3)` | +| `allocator_family_mismatch` | `std::malloc(sizeof(Widget))` followed by `delete` | `std::make_unique()` | +| `placement_new_mismatch` | placement `new` into `buffer`, then `delete p` | call `p->~Widget()`; `buffer` owns the storage | + +## Why it fails + +The defect is undefined behavior: each unsafe variant invokes the wrong destruction or deallocation protocol. The `malloc` case also has no constructed `Widget` before `delete`; the placement case tries to free stack storage that no allocation function returned. + +## Correct direction + +```cpp +auto objects = std::make_unique(3); +auto one = std::make_unique(); +Widget* placed = new (buffer) Widget{}; +placed->~Widget(); +``` + +Prefer RAII owners that encode the deallocator. When using placement `new`, write the explicit destructor call at the same abstraction level as the construction. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | often reports alloc/dealloc mismatch or invalid free | +| Debug CRT heap checks | can stop on mismatched heap operations in Debug builds | +| Compiler warnings | partial; runtime allocation families are generally hard to prove statically | +| Debugger address inspection | useful for placement `new`, but it does not enforce the contract | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session showing array, allocator-family, and placement-new evidence. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 17. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 14. diff --git a/Memory/Trap19_DoubleFree/README.md b/Memory/Trap19_DoubleFree/README.md new file mode 100644 index 0000000..ca64835 --- /dev/null +++ b/Memory/Trap19_DoubleFree/README.md @@ -0,0 +1,48 @@ +# Trap 19 — Double free + +**Rule:** Each dynamically allocated resource must have exactly one owning release path. + +## The rule + +A deallocation consumes the ownership right for that allocation. Raw pointers are only numeric values, so deleting through one pointer does not clear aliases, and constructing two owners from the same raw pointer does not create shared ownership. + +The allocator often reports the second release, but the bug begins earlier: when the program duplicates ownership or keeps using a pointer after its ownership has been spent. + +## In this code + +`main.cpp` runs three short variants. The safe target is `Trap19_DoubleFree`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap19_DoubleFree_unsafe`. + +| Function | Unsafe form | Safe form | +|---|---|---| +| `explicit_double_delete` | `delete p; delete p;` on the same raw pointer | `std::unique_ptr` with one `reset()` | +| `duplicated_ownership` | constructs `first(raw)` and `second(raw)` | moves ownership from `first` to `second` | +| `shallow_copy_double_free` | unsafe `Buffer` copy duplicates `data` | safe build deep-copies `Buffer::data` | + +## Why it fails + +The unsafe variants are undefined behavior. The same allocation is released twice, either explicitly, through two `unique_ptr` destructors that both believe they are exclusive owners, or through two `Buffer` destructors after a shallow copy. + +## Correct direction + +```cpp +auto first = std::make_unique(7); +auto second = std::move(first); +if (second) { use(*second); } +``` + +Make the ownership transfer visible in the type system. For classes, prefer Rule of Zero members or implement copying as a deep copy. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | reports many double frees with allocation and first-free context | +| Debug CRT heap checks | can show freed-memory patterns or stop at the second release | +| Compiler | prevents some cases when ownership is represented with non-copyable types | +| Release build | may be silent until allocator metadata is damaged later | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session locating duplicate owner addresses before the allocator failure. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 19. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 15 and practical exercise 5. diff --git a/Memory/Trap31_ShallowCopy/README.md b/Memory/Trap31_ShallowCopy/README.md new file mode 100644 index 0000000..c57bc0f --- /dev/null +++ b/Memory/Trap31_ShallowCopy/README.md @@ -0,0 +1,53 @@ +# Trap 31 — Shallow copy + +**Rule:** A class that owns a raw resource must define or disable copying, or delegate ownership to a member that does. + +## The rule + +If a class declares a destructor but leaves the copy constructor and copy assignment operator implicit, the compiler still generates memberwise copy operations. For a raw owning pointer, memberwise copy duplicates only the pointer value, not the allocation it owns. + +That gives two objects the same cleanup responsibility. The later double free is a symptom; the design error is the shallow copy of ownership. + +## In this code + +`main.cpp` runs three short variants. The safe target is `Trap31_ShallowCopy`. Because the source contains `RUN_UNSAFE_EXAMPLE`, enabling `TRAPS_BUILD_UNSAFE` also creates `Trap31_ShallowCopy_unsafe`. + +| Function | Class | Meaning | +|---|---|---| +| `shallow_copy_double_free` | `Broken` | unsafe build copies `Broken::data`; safe build skips the broken copy | +| `deep_copy_is_safe` | `RuleOfThree` | copy constructor duplicates the buffer and assignment uses copy-and-swap | +| `rule_of_zero` | `RuleOfZero` | `std::string data_` owns memory, so generated special members are correct | + +`ctest -R Safe_DeepCopy` runs the safe target and checks the root CMake PASS_REGULAR_EXPRESSION for the deep-copy output. + +## Why it fails + +The unsafe `Broken` path has undefined behavior at destruction because both copied objects own the same `char[]` and both destructors call `delete[]`. The copy itself is well formed; the ownership semantics are wrong. + +## Correct direction + +```cpp +class RuleOfZero { +public: + explicit RuleOfZero(std::string text) : data_(std::move(text)) {} +private: + std::string data_; +}; +``` + +Prefer Rule of Zero. If a raw resource is unavoidable, define destructor, copy constructor, and copy assignment together, and add move operations deliberately. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | reports the eventual double free in the unsafe build | +| Debugger pointer comparison | shows `a.data` and `b.data` are identical in the shallow copy | +| Compiler warnings | partial; generated copying is legal unless made unavailable or suspicious | +| Safe `ctest` | proves the repaired target prints the deep-copy line, not that every raw owner is safe | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session comparing shallow pointer identity with Rule-of-Three and Rule-of-Zero repairs. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 31. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 25 and practical exercise 8. \ No newline at end of file diff --git a/Memory/Trap33_MapBracketInsert/README.md b/Memory/Trap33_MapBracketInsert/README.md new file mode 100644 index 0000000..dd5c901 --- /dev/null +++ b/Memory/Trap33_MapBracketInsert/README.md @@ -0,0 +1,53 @@ +# Trap 33 — Map bracket insert + +**Rule:** `std::map::operator[]` is lookup-or-insert, not a read-only lookup. + +**This trap is not undefined behavior.** The insertion on a missing key is defined library behavior, so sanitizer silence is the expected, correct outcome. + +## The rule + +For associative containers such as `std::map`, `operator[]` must return a mutable reference to a mapped value. If the key is absent, the only way to return such a reference is to create an element with that key and a value-initialized mapped value. + +Read-only lookup uses different APIs: `find`, `contains`, and `at`. A `const std::map` deliberately has no `operator[]`, because the operation may mutate the container. + +## In this code + +`main.cpp` runs three short variants. There is no `_unsafe` target because the source does not contain `RUN_UNSAFE_EXAMPLE`; the trap shows wrong and correct forms directly in the normal target `Trap33_MapBracketInsert`. + +| Function | Form | Effect | +|---|---|---| +| `bracket_inserts_silently` | `scores["bob"] == 0` | inserts `bob` with value `0` while looking like a read | +| `non_mutating_lookups` | `find`, `contains`, and `at` | reads `scores` without inserting `bob` | +| `const_map_forbids_bracket` | `const std::map` and `at` | makes accidental bracket lookup a compile-time error; uses `histogram[c]` when insertion is intended | + +`ctest -R Safe_MapLookup` runs this target and checks the root CMake PASS_REGULAR_EXPRESSION for the non-mutating lookup size. + +## Why it fails + +The category is defined-but-wrong behavior. The library does exactly what `operator[]` specifies, but the program violates its higher-level invariant that a lookup should not change the map. + +## Correct direction + +```cpp +if (auto it = scores.find("bob"); it != scores.end()) { + use(it->second); +} +if (scores.contains("ada")) { use(scores.at("ada")); } +``` + +Use `operator[]` only when insert-or-update is the intended operation, as in the `histogram` loop. + +## Detection + +| Tool | Result | +|---|---| +| Assertions on `size()` | yes; they expose mutation across a supposed lookup | +| Debugger/container inspection | yes; `bob` appears after the bracket expression | +| AddressSanitizer / UBSan | no — there is no memory error or undefined operation | +| Compiler | helps if the map is `const`; otherwise the mutating call is valid | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session measuring map size before and after bracket lookup. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 33. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 27. \ No newline at end of file diff --git a/Memory/Trap40_FloatComparison/README.md b/Memory/Trap40_FloatComparison/README.md new file mode 100644 index 0000000..c21d240 --- /dev/null +++ b/Memory/Trap40_FloatComparison/README.md @@ -0,0 +1,55 @@ +# Trap 40 — Float comparison + +**Rule:** Compare floating-point values according to the numeric invariant, not by assuming decimal-looking values are exact. + +**This trap is not undefined behavior.** The comparisons and NaN behavior are defined floating-point semantics, so sanitizer silence is the expected, correct outcome. + +## The rule + +Most decimal fractions, including `0.1`, `0.2`, and `0.3`, are not exactly representable as binary floating-point values. Arithmetic rounds to nearby representable values, so the stored result of `0.1 + 0.2` can differ from the stored literal `0.3`. + +A tolerance must fit the scale of the values being compared. NaN is a separate rule: it is unordered and compares unequal to every value, including itself. + +## In this code + +`main.cpp` runs three short variants. There is no `_unsafe` target because the source does not contain `RUN_UNSAFE_EXAMPLE`; the trap shows correct rules and wrong equality assumptions directly in `Trap40_FloatComparison`. + +| Function | Demonstration | Correct idea | +|---|---|---| +| `exact_equality_fails` | prints `sum` for `0.1 + 0.2` and compares it with `0.3` | inspect or tolerate representation error | +| `tolerant_comparison` | calls `nearly_equal` for small and large magnitudes | combine absolute and relative tolerance | +| `accumulation_and_nan` | compares repeated accumulation with `1.0`, then tests `nan_value` | use integer loop counters and `std::isnan` | + +`ctest -R Safe_FloatTolerance` runs this target and checks the root CMake PASS_REGULAR_EXPRESSION for the NaN comparison line. + +## Why it fails + +The category is defined-but-wrong behavior. The program is not corrupt; the exact comparison encodes a false numeric invariant, and `nan_value == nan_value` is specified to be false. + +## Correct direction + +```cpp +if (nearly_equal(measured, expected)) { + accept(); +} +if (std::isnan(value)) { + handle_missing_number(); +} +``` + +Use a domain-appropriate tolerance, and treat non-finite values explicitly. For loop counts, keep the controlling variable integral when possible. + +## Detection + +| Tool | Result | +|---|---| +| High-precision printing / debugger bits | yes; shows adjacent or rounded representable values | +| Unit tests with boundary cases | yes; large magnitudes and NaN reveal bad equality assumptions | +| AddressSanitizer / UBSan | no — the arithmetic and comparisons are defined | +| Compiler warnings | usually no; exact comparison can be intentional in some domains | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session inspecting stored double bits, tolerance math, accumulation, and NaN. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 40. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 34. \ No newline at end of file diff --git a/Memory/Trap41_UnsignedUnderflow/README.md b/Memory/Trap41_UnsignedUnderflow/README.md new file mode 100644 index 0000000..e9149c8 --- /dev/null +++ b/Memory/Trap41_UnsignedUnderflow/README.md @@ -0,0 +1,55 @@ +# Trap 41 — Unsigned underflow + +**Rule:** Do not express possibly negative counts or indices in an unsigned type and then expect them to become negative. + +**This trap is not undefined behavior.** Unsigned wrap and signed-to-unsigned conversion are defined modular arithmetic, so sanitizer silence is the expected, correct outcome. + +## The rule + +Unsigned integer arithmetic is performed modulo one more than the maximum value of the type. For `std::size_t`, subtracting one from zero produces the maximum `std::size_t` value, not `-1`. + +The usual arithmetic conversions also matter. When a signed negative value is compared with an unsigned value of at least the same rank, the signed value is converted to unsigned first, which can turn `-1` into a huge value. + +## In this code + +`main.cpp` runs three short variants. There is no `_unsafe` target because the source does not contain `RUN_UNSAFE_EXAMPLE`; the trap shows safe forms and wrong commented forms directly in `Trap41_UnsignedUnderflow`. + +| Function | Wrong invariant | Safe form | +|---|---|---| +| `size_minus_one_on_empty` | `empty.size() - 1` should mean no last element | guard with `empty.empty()` before indexing | +| `reverse_loop_never_ends` | `i >= 0` can stop an unsigned reverse loop | use `for (std::size_t i = v.size(); i-- > 0;)` or reverse iterators | +| `signed_unsigned_comparison` | `-1 < 3u` behaves like signed comparison | compare in a chosen signed domain or use `std::ssize` | + +`ctest -R Safe_UnsignedGuard` runs this target and checks the root CMake PASS_REGULAR_EXPRESSION for the empty-container guard. + +## Why it fails + +The category is defined-but-wrong behavior. The arithmetic and conversions are specified, but the resulting huge value or false comparison violates the range invariant the code intended. + +## Correct direction + +```cpp +if (!v.empty()) { + use(v.back()); +} +for (auto it = v.rbegin(); it != v.rend(); ++it) { + use(*it); +} +``` + +Guard the container state before subtracting, use reverse iterators when possible, and use `std::ssize` when signed indexing is the clearest expression of the invariant. + +## Detection + +| Tool | Result | +|---|---| +| Assertions on invariants | yes; check `!empty()` before computing a last index | +| Compiler warnings | can report always-true unsigned comparisons or signed/unsigned mixes | +| AddressSanitizer / UBSan | no — unsigned wrap and conversion are defined; no bad access occurs in the safe code | +| Debugger/watch window | useful for seeing the maximum `std::size_t` value, but it is not a sanitizer failure | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB session inspecting wrap, reverse-loop control, and signed/unsigned conversion. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 41. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — questions 6 and 35 and practical exercise 12. \ No newline at end of file diff --git a/ObjectModel/Trap10_VirtualInConstructor/README.md b/ObjectModel/Trap10_VirtualInConstructor/README.md new file mode 100644 index 0000000..425fd9f --- /dev/null +++ b/ObjectModel/Trap10_VirtualInConstructor/README.md @@ -0,0 +1,53 @@ +# Trap 10 — Virtual in Constructor + +**Rule:** virtual dispatch during construction or destruction is limited to the class whose constructor or destructor is currently running. + +**This trap is not undefined behavior.** `Base::Base()` calling `Base::speak()` is defined dispatch to the base implementation, so sanitizer silence is the expected result. + +## The rule + +While a base subobject is being constructed, the most-derived object is not yet active as a complete `Derived`. The virtual-call rule reflects that phase: a virtual call made from `Base::Base()` resolves as if the dynamic type were `Base`, not the final type that will exist after all constructors finish. + +This protects the program from calling an override that expects derived members to be initialized. It also means virtual functions are not an initialization customization point. + +## In this code + +`main.cpp` constructs a `Derived d`. The `Base` constructor calls `speak()`, and that call prints `Base phase` because the `Derived` part is not active yet. After construction, `main` calls `d.speak()`, which dispatches to `Derived::speak()` and uses `ready`. + +- **Safe target** (`Trap10_VirtualInConstructor`) — shows the defined constructor-phase dispatch rule directly. +- There is no `_unsafe` target. The source contains no `RUN_UNSAFE_EXAMPLE` because this virtual call is not UB; it is defined dispatch with often-surprising semantics. + +## Why it fails + +The defect is defined-but-dangerous design. Code that expects `Base::Base()` to call `Derived::speak()` is relying on a dynamic type that does not exist yet. The derived member `ready` is not a valid dependency of base construction. + +## Correct direction + +```cpp +struct Base { + Base() = default; + virtual ~Base() = default; + virtual void speak() {} +}; + +struct Derived : Base { + int ready{99}; + void initialize() { speak(); } +}; +``` + +Finish construction first, then call virtual customization from a separate step or factory. Keep base constructors responsible only for base invariants. + +## Detection + +| Tool | Result | +|---|---| +| Debugger call stack / vftable inspection | shows `Base::speak()` during `Base::Base()` and `Derived::speak()` after construction | +| Compiler warnings | generally no; the call is well formed and defined | +| ASan / UBSan | no report, because there is no memory or lifetime violation in this source | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing the constructor-phase vftable change. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 10. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 22. diff --git a/ObjectModel/Trap11_NonVirtualDestructor/README.md b/ObjectModel/Trap11_NonVirtualDestructor/README.md new file mode 100644 index 0000000..4e29a31 --- /dev/null +++ b/ObjectModel/Trap11_NonVirtualDestructor/README.md @@ -0,0 +1,48 @@ +# Trap 11 — Non-Virtual Destructor + +**Rule:** a polymorphic base that is deleted through a base pointer needs a virtual destructor, or deletion through that base type has undefined behavior. + +## The rule + +Deleting an object through a pointer to base must use the same destruction contract that created the complete object. If the static type has a non-virtual destructor, the delete expression cannot dispatch to the derived destructor, so the standard makes that operation undefined behavior when the dynamic object is derived. + +A base class can also forbid polymorphic deletion by making its destructor protected and non-virtual. What it must not do is offer public base-pointer deletion while omitting the virtual destructor. + +## In this code + +`main.cpp` demonstrates the correct form. `Base` declares `virtual ~Base() = default`, `Derived` owns a `std::unique_ptr resource`, and `std::unique_ptr p` is initialized with `std::make_unique()`. + +- **Safe target** (`Trap11_NonVirtualDestructor`) — destruction through `std::unique_ptr` dispatches through `Base`'s virtual destructor. +- There is no `_unsafe` target. The source contains no `RUN_UNSAFE_EXAMPLE` because this folder compiles the supported virtual-destructor form rather than a deliberately broken base class. + +## Why it fails + +The broken version would be undefined behavior: `std::unique_ptr` would perform deletion through `Base*` while the complete object is `Derived`. Without a virtual destructor, the derived cleanup contract is missing, so `Derived::resource` cleanup is not something the program may rely on. + +## Correct direction + +```cpp +struct Base { + virtual ~Base() = default; +}; + +struct Derived final : Base { + std::unique_ptr resource = std::make_unique(42); +}; +``` + +Use a public virtual destructor when clients may own derived objects through `Base*` or `std::unique_ptr`. If base-pointer deletion is not supported, make that impossible in the interface. + +## Detection + +| Tool | Result | +|---|---| +| Compiler warnings | often warn for deleting a polymorphic object through a base with a non-virtual destructor | +| Leak tools / destructor breakpoints | show skipped derived cleanup in broken variants | +| ASan | not a general detector for missing virtual destructors; the wrong delete may not immediately touch invalid memory | +| This repository target | safe by construction; `Base` has a virtual destructor | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing the destructor dispatch path. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 11. diff --git a/ObjectModel/Trap12_ObjectSlicing/README.md b/ObjectModel/Trap12_ObjectSlicing/README.md new file mode 100644 index 0000000..a883952 --- /dev/null +++ b/ObjectModel/Trap12_ObjectSlicing/README.md @@ -0,0 +1,55 @@ +# Trap 12 — Object Slicing + +**Rule:** copying a derived object into a base object creates a new standalone base object; the derived part is not copied into the destination. + +**This trap is not undefined behavior.** Slicing is a well-defined copy of the base subobject, so sanitizer silence is the expected result. + +## The rule + +A `Base` object has exactly the state and dynamic type of `Base`. When a `Derived` is used to initialize a `Base` by value, only the `Base` subobject participates in that copy. Any derived state, override identity, or invariant outside the base subobject is not part of the destination. + +Polymorphism requires indirection: a reference, pointer, or owning handle to a base subobject. Value semantics of the base type deliberately produce base values. + +## In this code + +`main.cpp` runs three variants: + +| Function | Slicing form | Correct form | +|---|---|---| +| `copy_slicing` | `Base sliced = d;` then `sliced.type()` | `const Base& polymorphic = d;` | +| `container_slicing` | `_unsafe`: `std::vector` and `push_back(Derived{})` | `std::vector>` | +| `parameter_slicing` | `_unsafe`: `print_by_value(Base b)` | `print_by_reference(const Base& b)` | + +- **Safe target** (`Trap12_ObjectSlicing`) — keeps polymorphic objects behind references or owning pointers where needed. +- **Unsafe target** (`Trap12_ObjectSlicing_unsafe`, `RUN_UNSAFE_EXAMPLE`) — shows the same defined slicing in container and parameter forms. + +## Why it fails + +The category is defined-but-wrong behavior. The program is not corrupt; it simply asked for a `Base` value and got one. Dynamic dispatch then calls `Base::type()` because the destination object's dynamic type is `Base`. + +## Correct direction + +```cpp +void print_by_reference(const Base& b) { + b.type(); +} + +std::vector> values; +values.push_back(std::make_unique()); +``` + +Pass polymorphic objects by reference or pointer. Store polymorphic objects through owning handles rather than by value in a base-typed container. + +## Detection + +| Tool | Result | +|---|---| +| Debugger dynamic-type / vftable inspection | shows the copied object has `Base` dynamic type | +| Code review | effective when looking for base-by-value parameters and containers | +| ASan / UBSan | no report, because slicing is a valid copy | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session comparing the sliced object and the referenced derived object. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 12. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 13 and practical exercise 4. diff --git a/ObjectModel/Trap13_StrictAliasing/README.md b/ObjectModel/Trap13_StrictAliasing/README.md new file mode 100644 index 0000000..ea8e594 --- /dev/null +++ b/ObjectModel/Trap13_StrictAliasing/README.md @@ -0,0 +1,45 @@ +# Trap 13 — Strict Aliasing + +**Rule:** a cast changes the type of the *pointer*, not the type of the object stored at that address. + +## The rule + +An object may be read only through a type compatible with its dynamic type (plus a few explicitly permitted forms, such as `char`, `unsigned char`, and `std::byte`). Reading a `float` object through a `std::uint32_t*` is not one of them, so the access is undefined behavior even though both types are four bytes wide and the address is perfectly aligned. + +Compilers rely on this rule to prove that a `float*` and a `std::uint32_t*` cannot refer to the same object, which lets them reorder, cache, or eliminate loads and stores. That is why the defect is optimizer-dependent: `/Od` often produces the expected number and `/O2` does not. + +## In this code + +`main.cpp` extracts the bit pattern of `1.0F` two ways: + +- **Safe target** (`Trap13_StrictAliasing`) — `std::bit_cast(value)`, a defined value-representation copy between equal-sized trivially copyable types. +- **Unsafe target** (`Trap13_StrictAliasing_unsafe`, `RUN_UNSAFE_EXAMPLE`) — `*reinterpret_cast(&value)`, a typed access the object model does not permit. + +Both print `3f800000` in a typical Debug build. That agreement is the trap: it is evidence about one build, not about the language rule. + +## Why it fails + +`reinterpret_cast` produces a pointer of the requested type; it does not create a `std::uint32_t` object at that address and does not end the `float`'s lifetime. Dereferencing it reads an object through an incompatible type. Because the behavior is undefined, the compiler may assume the two pointers never alias and keep `value` in a register while the read observes stale memory — or vice versa. + +## Correct direction + +```cpp +auto bits = std::bit_cast(value); // C++20, constexpr-friendly +std::memcpy(&bits, &value, sizeof bits); // pre-C++20 equivalent +``` + +Both copy the value representation without claiming that one object is another. Reading through `std::byte`/`unsigned char` is also permitted when inspecting raw bytes is the actual intent. + +## Detection + +| Tool | Result | +|---|---| +| Comparing `/Od` and `/O2` output | the most reliable signal; a value that changes with optimization | +| GCC/Clang `-Wstrict-aliasing` | sometimes warns, and misses many real cases | +| UndefinedBehaviorSanitizer | may flag related type-confusion, but does not diagnose aliasing generally | +| AddressSanitizer | no — memory is in bounds and alive; the violation is in the type system | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session comparing the generated loads. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 13. diff --git a/ObjectModel/Trap14_Alignment/README.md b/ObjectModel/Trap14_Alignment/README.md new file mode 100644 index 0000000..b6964b6 --- /dev/null +++ b/ObjectModel/Trap14_Alignment/README.md @@ -0,0 +1,49 @@ +# Trap 14 — Alignment + +**Rule:** a typed access is valid only when the address satisfies that type's alignment requirement. + +## The rule + +Every object type has an alignment requirement, reported by `alignof(T)`. Creating or using a `T*` for storage that is not suitably aligned for `T` violates the object model, even if the address lies inside a live byte buffer and even if the hardware happens to tolerate the load or store. + +Alignment is a language requirement, not just a performance hint. Some architectures fault on misaligned access, while others execute it more slowly; the C++ program is undefined either way. + +## In this code + +`main.cpp` allocates `alignas(std::uint64_t) std::byte storage[sizeof(std::uint64_t)+1]`. + +| Target | Pointer | Behavior | +|---|---|---| +| `Trap14_Alignment` | `reinterpret_cast(storage)` | address is aligned for `std::uint64_t`; writes value 7 and prints the read value | +| `Trap14_Alignment_unsafe` | `reinterpret_cast(storage + 1)` | address is one byte past the aligned base; typed access is misaligned UB | + +The `_unsafe` target exists because the source contains `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The unsafe branch has undefined behavior. The `std::byte` array provides storage, but `storage + 1` does not satisfy `alignof(std::uint64_t)`. A successful x64 Debug run only shows that this hardware/build accepted one misaligned instruction. + +## Correct direction + +```cpp +alignas(std::uint64_t) std::byte storage[sizeof(std::uint64_t)]; +auto* p = reinterpret_cast(storage); +std::construct_at(p, 7ULL); +std::destroy_at(p); +``` + +Use storage whose address is aligned for the destination type, and begin the object's lifetime before treating the bytes as a live object. + +## Detection + +| Tool | Result | +|---|---| +| Clang/GCC UBSan alignment checks | reports the misaligned typed access | +| Debugger address check | `address % alignof(std::uint64_t)` exposes the defect | +| MSVC AddressSanitizer | no; it checks addressability, not this alignment rule | +| x64 hardware behavior | may appear to work, which is not evidence of defined C++ | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session measuring the aligned and misaligned addresses. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 14. diff --git a/ObjectModel/Trap15_PointerProvenance/README.md b/ObjectModel/Trap15_PointerProvenance/README.md new file mode 100644 index 0000000..4fe6e96 --- /dev/null +++ b/ObjectModel/Trap15_PointerProvenance/README.md @@ -0,0 +1,49 @@ +# Trap 15 — Pointer Provenance + +**Rule:** a numeric address is not enough to prove that a pointer may access a live object. + +## The rule + +C++ pointer validity carries more information than the integer value printed for the address. The access must still be within the correct object's lifetime, bounds, alignment, and permitted typed-access rules. Converting a pointer to an integer and back is a platform boundary operation; it does not preserve ownership or extend lifetime. + +That distinction matters most when the original owner changes. A reconstructed pointer can compare equal to the old address while no live object remains there. + +## In this code + +`main.cpp` creates `auto owner = std::make_unique(42)` and saves `int* original = owner.get()`. + +| Target | Code path | Meaning | +|---|---|---| +| `Trap15_PointerProvenance` | `int* observer = original;` then reads while `owner` is alive | valid observation of a live allocation | +| `Trap15_PointerProvenance_unsafe` | stores `original` in `std::uintptr_t`, reconstructs `int*`, calls `owner.reset()`, then dereferences | numeric address survives, object lifetime does not | + +The `_unsafe` target exists because the source contains `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The unsafe branch has undefined behavior. After `owner.reset()`, the `int` lifetime has ended and the allocation has been released. `reconstructed` may still hold the same numeric address, but it no longer denotes a live `int` that the program may read. + +## Correct direction + +```cpp +auto owner = std::make_unique(42); +int* observer = owner.get(); +std::cout << *observer << '\n'; // owner is still alive +``` + +Keep the owner alive for the full observation window. If access must outlive a scope, pass ownership or a validated weak/shared handle instead of an address-shaped integer. + +## Detection + +| Tool | Result | +|---|---| +| AddressSanitizer | reports this particular unsafe branch as use-after-free | +| Ownership/lifetime review | required for the general provenance question | +| Debugger numeric address comparison | can show equality, but cannot prove validity | +| Compiler warnings | generally none; the casts are syntactically valid | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session comparing the live owner path with the reconstructed pointer path. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 15. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 1. diff --git a/ObjectModel/Trap29_RawBytes/README.md b/ObjectModel/Trap29_RawBytes/README.md new file mode 100644 index 0000000..53303bb --- /dev/null +++ b/ObjectModel/Trap29_RawBytes/README.md @@ -0,0 +1,46 @@ +# Trap 29 — Raw Bytes + +**Rule:** suitably sized and aligned storage is not a live object until construction begins that object's lifetime. + +## The rule + +Raw storage and object lifetime are separate facts. `std::byte storage[sizeof(T)]` can reserve bytes with the right size, and `alignas(T)` can make the address suitable, but neither operation constructs a `T`. A typed pointer to that storage is only a candidate until lifetime is begun. + +For non-implicit-lifetime class types such as `Record`, use construction and destruction APIs that express the lifetime boundary. After destruction, the bytes remain, but the object no longer exists. + +## In this code + +`main.cpp` defines `Record` with `std::string name` and `int number`. It creates aligned byte storage, forms `candidate`, constructs a `Record` with `std::construct_at(candidate, 7)`, prints the constructed fields, and then calls `std::destroy_at(object)`. + +- **Safe target** (`Trap29_RawBytes`) — demonstrates the complete raw-storage protocol. +- There is no `_unsafe` target. The source contains no `RUN_UNSAFE_EXAMPLE`; it shows the correct rule rather than compiling a pre-lifetime typed access. + +## Why it fails + +The trap would be undefined behavior if code read `candidate->name` or `candidate->number` before `std::construct_at`, or after `std::destroy_at`. At those points the storage exists, but no live `Record` object exists there. + +## Correct direction + +```cpp +alignas(Record) std::byte storage[sizeof(Record)]; +auto* candidate = reinterpret_cast(storage); +Record* object = std::construct_at(candidate, 7); +std::destroy_at(object); +``` + +Treat `construct_at` and `destroy_at` as the lifetime markers. Do not confuse a byte address with a constructed class object. + +## Detection + +| Tool | Result | +|---|---| +| `ctest -R Safe_RawBytes` | checks that the constructed `Record` name and number appear | +| Debugger lifetime trace | shows the same storage before construction, during the live `Record`, and after destruction | +| ASan | usually no for pre-lifetime class access when storage is addressable | +| Compiler warnings | generally no; the pointer cast alone is not the lifetime violation | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session marking the raw-storage and live-object phases. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 29. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 23 and practical exercise 7. diff --git a/ObjectModel/Trap30_UnsequencedModification/README.md b/ObjectModel/Trap30_UnsequencedModification/README.md new file mode 100644 index 0000000..06e1dcd --- /dev/null +++ b/ObjectModel/Trap30_UnsequencedModification/README.md @@ -0,0 +1,50 @@ +# Trap 30 — Unsequenced Modification + +**Rule:** do not modify the same scalar object more than once in one full-expression unless the modifications are sequenced. + +## The rule + +C++ specifies sequencing relationships between evaluations, not a universal left-to-right evaluation order. If two side effects on the same scalar object are unsequenced relative to each other, or one side effect is unsequenced relative to a value computation of that same object, the behavior is undefined. + +Post-increment and pre-increment each modify their operand. Combining them in a larger expression does not automatically choose a portable order. + +## In this code + +`main.cpp` starts with `int value = 1`. + +| Target | Code path | Meaning | +|---|---|---| +| `Trap30_UnsequencedModification` | stores `old`, increments `value`, stores `after_first_increment`, increments again, then assigns `old + after_first_increment` | every state transition is sequenced | +| `Trap30_UnsequencedModification_unsafe` | `value = value++ + ++value;` | multiple unsequenced modifications of `value` | + +The `_unsafe` target exists because the source contains `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The unsafe branch has undefined behavior. There is no standard value for the expression, even if one compiler and build prints a stable number. The emitted instruction order is an implementation artifact, not a meaning assigned by C++. + +## Correct direction + +```cpp +const int old = value; +++value; +const int after_first_increment = value; +++value; +value = old + after_first_increment; +``` + +Give each mutation its own statement when the intermediate states matter. This makes the sequencing relationship explicit and reviewable. + +## Detection + +| Tool | Result | +|---|---| +| Clang/GCC warnings such as `-Wunsequenced` | often diagnose the compact expression | +| Code review | reliable when looking for repeated `++`, `--`, or assignments to the same scalar in one expression | +| UBSan | not a dependable detector for all unsequenced side effects | +| MSVC `/W4` in the debug analysis | did not warn for this source | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session separating the defined statements from one emitted unsafe order. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 30. diff --git a/ObjectModel/Trap34_VectorBool/README.md b/ObjectModel/Trap34_VectorBool/README.md new file mode 100644 index 0000000..171f220 --- /dev/null +++ b/ObjectModel/Trap34_VectorBool/README.md @@ -0,0 +1,53 @@ +# Trap 34 — `vector` Proxy + +**Rule:** `std::vector` is a packed specialization whose element access returns proxy objects, not real `bool&` references. + +**This trap is not undefined behavior.** The proxy operations are defined library behavior; compile errors and surprising aliasing are the expected symptoms, not sanitizer reports. + +## The rule + +Unlike `std::vector` for ordinary `T`, `std::vector` is permitted to pack bits instead of storing addressable `bool` objects. Because a single bit has no `bool*` address, `operator[]` returns a proxy reference object that knows how to read or write the selected bit. + +That proxy can be useful for compact flags, but it breaks assumptions about references, contiguous `bool` storage, and `auto` copies. + +## In this code + +`main.cpp` runs three variants: + +| Function | Demonstrated issue | Correct direction shown | +|---|---|---| +| `proxy_instead_of_reference` | `auto proxy = bits[0]` still aliases the container; `bool& ref = bits[0]` would not compile | write `bool copy = bits[2]` for a detached value | +| `no_contiguous_storage` | no `const bool* raw = bits.data()` and no `for (bool& b : bits)` | use `auto&& b` for proxies or `std::vector` for bytes | +| `better_alternatives` | packed bits are the wrong abstraction for some jobs | `std::bitset`, `std::array`, or `std::vector` | + +- **Safe target** (`Trap34_VectorBool`) — contains all three demonstrations directly. +- There is no `_unsafe` target because the source contains no `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The category is defined-but-wrong design, with some wrong forms rejected at compile time. Code that expects an addressable `bool` element or detached `auto` value is using the wrong container contract. + +## Correct direction + +```cpp +bool copy = bits[2]; // detached value +for (auto&& bit : bits) bit = false; // proxy-aware mutation +std::vector flags(8, 1); // contiguous addressable storage +``` + +Choose the container that matches the requirement: packed flags, fixed-size bits, real bool references, or byte-addressable interop storage. + +## Detection + +| Tool | Result | +|---|---| +| Compiler errors | catch attempts to bind `bool&` or use `bool*` storage | +| `ctest -R Safe_VectorBoolProxy` | checks that an explicit `bool` copy is detached from the proxy | +| Debugger type inspection | shows `auto proxy` is a proxy type, not `bool` | +| ASan / UBSan | no report, because the proxy behavior is valid library behavior | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing the proxy object and packed storage. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 34. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 28. diff --git a/ObjectModel/Trap36_HiddenOverload/README.md b/ObjectModel/Trap36_HiddenOverload/README.md new file mode 100644 index 0000000..ee54bf3 --- /dev/null +++ b/ObjectModel/Trap36_HiddenOverload/README.md @@ -0,0 +1,54 @@ +# Trap 36 — Hidden Overload + +**Rule:** declaring a member with a name in a derived class hides every base-class overload with that same name. + +**This trap is not undefined behavior.** Name hiding is a compile-time overload-resolution rule, so sanitizer silence is the expected result. + +## The rule + +Unqualified lookup finds declarations by name before overload resolution chooses among signatures. When `Hiding` declares `log(double)`, lookup for `h.log(...)` stops in `Hiding`; the `Base::log(int)` and `Base::log(const std::string&)` overloads are not candidates unless they are explicitly qualified or reintroduced. + +The same idea protects virtual overrides: a signature mismatch is not an override. The `override` keyword turns that mistake into a compile error. + +## In this code + +`main.cpp` runs three variants: + +| Function | Wrong form | Correct form | +|---|---|---| +| `name_hiding_changes_overload_resolution` | `h.log(1)` converts to `Hiding::log(double)`; `h.log("text")` would not compile | `h.Base::log(1)` reaches the base explicitly | +| `using_declaration_restores_the_set` | — | `using Base::log;` makes all base overloads visible in `Exposing` | +| `override_keyword_catches_mismatch` | commented `draw(long) const override` would fail | `Circle::draw(int) const override` matches `Shape::draw` | + +- **Safe target** (`Trap36_HiddenOverload`) — demonstrates hiding and the repairs directly. +- There is no `_unsafe` target because the source contains no `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The category is a compile-time lookup and design error. The base overloads exist, but they are not in the overload set selected for `Hiding`. Overload resolution cannot choose a function that name lookup never found. + +## Correct direction + +```cpp +struct Exposing : Base { + using Base::log; + void log(double value); +}; +``` + +Add a `using` declaration when a derived class intentionally extends a base overload set. Use `override` on virtual functions so signature drift is diagnosed immediately. + +## Detection + +| Tool | Result | +|---|---| +| `ctest -R Safe_HiddenOverload` | checks that the restored overload set selects `Base::log(int)` for the `int` argument | +| Compiler | rejects calls such as the commented hidden string overload and catches bad `override` signatures | +| Debugger call stack | shows `h.log(1)` resolved to `Hiding::log(double)` | +| ASan / UBSan | no report, because no runtime memory rule is violated | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing which overload each call resolved to. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 36. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 30. diff --git a/ObjectModel/Trap37_VirtualDefaultArg/README.md b/ObjectModel/Trap37_VirtualDefaultArg/README.md new file mode 100644 index 0000000..a7d23a8 --- /dev/null +++ b/ObjectModel/Trap37_VirtualDefaultArg/README.md @@ -0,0 +1,55 @@ +# Trap 37 — Virtual Default Argument + +**Rule:** virtual dispatch selects the function body dynamically, but default arguments are substituted from the static type at the call site. + +**This trap is not undefined behavior.** The calls are fully defined; the surprising result is the specified split between static defaults and dynamic dispatch. + +## The rule + +Default arguments are not virtual. They are compile-time substitutions made where the call is written. After that substitution, the virtual call mechanism chooses the final overrider using the object's dynamic type. + +Putting different defaults on overrides therefore creates one function body that can receive different implicit argument values depending on the expression's static type. + +## In this code + +`main.cpp` runs three variants: + +| Function | Demonstrated issue | Correct direction shown | +|---|---|---| +| `default_argument_comes_from_static_type` | `d.render()` uses `Derived`'s default `100`; `as_base.render()` calls `Derived::render` with `Base`'s default `1` | avoid different defaults on virtual functions | +| `non_virtual_interface_has_one_default` | — | `Interface::render(int scale = 1)` is non-virtual and calls virtual `do_render` | +| `overloads_instead_of_defaults` | — | `Explicit::render()` forwards to `render(1)` | + +- **Safe target** (`Trap37_VirtualDefaultArg`) — contains the wrong and corrected designs directly. +- There is no `_unsafe` target because the source contains no `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The category is defined-but-wrong behavior. The programmer expects the override's default to travel with the override body, but the default was already chosen from the static type before virtual dispatch happened. + +## Correct direction + +```cpp +struct Interface { + void render(int scale = 1) const { do_render(scale); } +private: + virtual void do_render(int scale) const = 0; +}; +``` + +Keep defaults on a non-virtual wrapper, or use overloads instead of defaults. The virtual function itself should receive explicit arguments. + +## Detection + +| Tool | Result | +|---|---| +| `ctest -R Safe_VirtualDefault` | checks that the base-reference call reaches the derived override with scale 1 | +| Code review | effective when looking for default arguments repeated on overrides | +| Debugger argument inspection | shows `Derived::render` reached with both `100` and `1` | +| ASan / UBSan | no report, because the behavior is defined | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing dynamic body selection with static default values. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 37. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 31. diff --git a/ObjectModel/Trap38_MostVexingParse/README.md b/ObjectModel/Trap38_MostVexingParse/README.md new file mode 100644 index 0000000..b083a29 --- /dev/null +++ b/ObjectModel/Trap38_MostVexingParse/README.md @@ -0,0 +1,53 @@ +# Trap 38 — Most Vexing Parse + +**Rule:** if a statement can be parsed as a declaration, C++ parses it as a declaration. + +**This trap is not undefined behavior.** The wrong forms are parsing or compile-time issues, so sanitizer silence is the expected result. + +## The rule + +C++ grammar gives declarations priority in ambiguous-looking constructs. Inside a function, `Timer t();` is not a default-constructed local object; it declares a function named `t` that takes no parameters and returns `Timer`. + +Braces often remove that declaration interpretation, but braces have their own overload rule: initializer-list constructors are preferred when available. + +## In this code + +`main.cpp` runs three variants: + +| Function | Wrong or surprising form | Correct form | +|---|---|---| +| `empty_parens_declare_a_function` | `Timer t();` declares a function; `t.ticks` would not compile | `Timer braced{};` or `Timer plain;` | +| `named_argument_becomes_a_parameter` | commented iterator constructor shape can declare a function | named iterator values or `std::vector braced_form{...}` | +| `braces_have_their_own_rule` | `std::vector braces{3, 0}` creates two elements, not three zeros | `std::vector parens(3, 0)` for the size constructor | + +- **Safe target** (`Trap38_MostVexingParse`) — includes the parse trap and the safe alternatives directly. +- There is no `_unsafe` target because the source contains no `RUN_UNSAFE_EXAMPLE`. + +## Why it fails + +The category is a parsing and compile-time error. No `Timer` object named `t` exists in the first variant, so member access would fail to compile. In the vector example, braces select a different valid constructor. + +## Correct direction + +```cpp +Timer braced{}; +Timer plain; +std::vector zeros(3, 0); +``` + +Use braces or no parentheses for default construction. Use parentheses when you specifically want a size/value constructor that would conflict with an initializer-list overload. + +## Detection + +| Tool | Result | +|---|---| +| MSVC C4930 / Clang and GCC `-Wvexing-parse` | warn that a declaration was probably intended as an object definition | +| `ctest -R Safe_VexingParse` | checks that the braced and plain `Timer` objects both have ticks 7 | +| Compiler errors | appear when trying to use `t` as an object | +| ASan / UBSan | no report, because the problem is not a runtime memory error | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session confirming no local `t` object exists. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 38. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 32. diff --git a/ObjectModel/Trap39_MemberInitOrder/README.md b/ObjectModel/Trap39_MemberInitOrder/README.md new file mode 100644 index 0000000..656bed3 --- /dev/null +++ b/ObjectModel/Trap39_MemberInitOrder/README.md @@ -0,0 +1,53 @@ +# Trap 39 — Member Init Order + +**Rule:** bases are initialized first, then members in declaration order, regardless of the order written in the constructor's init-list. + +## The rule + +The member-initializer list does not define construction order. It supplies initializers that are applied in the fixed order determined by the class definition: virtual bases, direct bases, then non-static data members in declaration order. + +That rule keeps destruction order well-defined, but it makes dependencies between members dangerous. A member initializer must not read a member declared later, because that later member has not been initialized yet. + +## In this code + +`main.cpp` runs three variants: + +| Function | Demonstrated form | Meaning | +|---|---|---| +| `init_list_order_is_a_lie` | `_unsafe`: `Reordered` declares `doubled` before `count` but initializes `doubled(count * 2)` | reads an indeterminate `count` | +| `safe_dependent_members` | `Ordered` declares `count` before `doubled`; `BodyComputed` assigns in the body | dependencies are sequenced safely | +| `bases_precede_members` | `View : Buffer` initializes `Buffer(n)` before `size(storage.size())` | base subobject is alive before members | + +- **Safe target** (`Trap39_MemberInitOrder`) — skips the UB `Reordered` construction and runs the safe forms. +- **Unsafe target** (`Trap39_MemberInitOrder_unsafe`, `RUN_UNSAFE_EXAMPLE`) — constructs `Reordered r{21}` and reads `count` before initialization. + +## Why it fails + +The unsafe branch has undefined behavior. `doubled` is initialized first because it is declared first, and its initializer reads `count` while `count` still has an indeterminate value. + +## Correct direction + +```cpp +struct Ordered { + int count; + int doubled; + explicit Ordered(int value) : count(value), doubled(count * 2) {} +}; +``` + +Declare members in dependency order, or compute dependent values in the constructor body after all members have been initialized. + +## Detection + +| Tool | Result | +|---|---| +| `ctest -R Safe_MemberInitOrder` | checks that `Ordered` has count 21 and doubled 42 | +| Clang/GCC `-Wreorder` and related warnings | can flag init-list order or dependency mistakes | +| MemorySanitizer | can detect the indeterminate read in suitable builds | +| ASan | no; the read is in bounds and from live storage, but the value is indeterminate | + +## Next + +- [`debug_analysis.md`](debug_analysis.md) — CDB/WinDbg session showing constructor order and the indeterminate read. +- [`../../TRAP_GUIDE.md`](../../TRAP_GUIDE.md) — row 39. +- [`../../SELF_TEST.md`](../../SELF_TEST.md) — question 33 and practical exercise 11. diff --git a/README.md b/README.md index 2a020c1..75ba179 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,18 @@ This project was created through collaboration between **Bugra Postaci** and mul Opening the repository folder keeps Solution Explorer attached to the tracked source tree, so Visual Studio's Git status decorations remain meaningful. It also exposes CMake presets directly and connects the CTest tests to Test Explorer. Do not open a generated solution under `VisualStudio/` or another build directory as the primary workspace. -### Per-trap WinDbg/CDB analysis +### What is inside a trap folder -Every `TrapXX_*` folder also contains two equivalent, trap-specific native-debugger notebooks: +Every `TrapXX_*` folder contains the source plus three documents with a deliberate division of labour: -- `debug_analysis.md` - the readable walkthrough with the source excerpt, build commands, breakpoint plan, CDB/WinDbg commands, expected evidence, and interpretation. +- `README.md` - the concept. The language rule, what the code demonstrates, why it fails, the corrective pattern, and which tools do and do not detect it. GitHub renders this automatically when you open the folder, so it is the entry point. It contains no debugger commands. +- `debug_analysis.md` - the evidence. The readable walkthrough with the source excerpt, build commands, breakpoint plan, CDB/WinDbg commands, expected output, and interpretation. - `debug_analysis.txt` - the same session in a console-friendly plain-text form for copying commands while CDB is open. +Read `README.md` to learn the rule, then `debug_analysis.md` to prove it in a debugger. + +### Per-trap WinDbg/CDB analysis + These files are not generic debugger notes. Each one is written for that trap's actual target, `BP:` markers, symbols, invariants, and safe/unsafe distinction. Start with the Markdown notebook beside `main.cpp`; use the text version when running the command-line session. The repository therefore supports two complementary paths: Visual Studio for interactive source debugging and CDB/WinDbg for a reproducible command-and-evidence session. Projects that contain an anti-pattern demonstration have a separately generated target ending in `_unsafe`. For example, use `Trap01_UseAfterFree` to study the safe path and `Trap01_UseAfterFree_unsafe` to enter the guarded invalid access. Do not add `RUN_UNSAFE_EXAMPLE` manually; CMake defines it only for these explicit targets. Depending on the variant, an `_unsafe` branch may demonstrate undefined behavior, a lifetime error, an unspecified-but-valid state, or defined behavior that is surprising and error-prone. Prefer AddressSanitizer for memory-unsafe targets; do not expect a sanitizer to diagnose every logic or API-contract mistake. diff --git a/TRAP_GUIDE.md b/TRAP_GUIDE.md index 6bcfb24..1f250b7 100644 --- a/TRAP_GUIDE.md +++ b/TRAP_GUIDE.md @@ -2,6 +2,7 @@ Every trap directory represents one canonical book topic; selected directories contain several named variants of that topic, and advanced build traps may contain multiple translation units. Comments explain the questionable operation beside the relevant statement and identify the corrective pattern. When a source contains a guarded `RUN_UNSAFE_EXAMPLE` branch, CMake creates a separately named `_unsafe` target. Traps without such a branch have only their normal target. `_unsafe` is a teaching label: a branch can be undefined behavior, a lifetime violation, an unspecified-but-valid state, or defined yet dangerous logic. +This page is the index. Each row summarises one trap in a single line; the `README.md` inside that trap's folder expands the same trap into the full rule, the corrective pattern, and the tools that do and do not detect it. | Trap | Category | Typical symptom | Why it fails | Detection | Correct direction | |---:|---|---|---|---|---| | 01 | Memory | delayed crash/corruption | access after dynamic lifetime | ASan | RAII ownership |