Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions ABI_Build/Trap18_AllocatorBoundary/README.md
Original file line number Diff line number Diff line change
@@ -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<void, FreeDeleter> 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<void, FreeDeleter> 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.
55 changes: 55 additions & 0 deletions ABI_Build/Trap25_ExceptionRAII/README.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions ABI_Build/Trap26_StaticInitOrder/README.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 55 additions & 0 deletions ABI_Build/Trap27_ODRViolation/README.md
Original file line number Diff line number Diff line change
@@ -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.
56 changes: 56 additions & 0 deletions ABI_Build/Trap28_ABIMismatch/README.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 51 additions & 0 deletions Concurrency/Trap20_DataRace/README.md
Original file line number Diff line number Diff line change
@@ -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<int> 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<int> 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.
56 changes: 56 additions & 0 deletions Concurrency/Trap21_CheckThenAct/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading