Undefined Behavior, Memory Bugs and Secure Coding

C and C++ trust the programmer completely: if you write past the end of an array, the language does not stop you.

Learning Objectives

Recognise every major memory-bug class: out-of-bounds access, stack smashing, heap corruption, use-after-free, double free, leaks, uninitialised reads, null dereference, dangling references, invalid free, format-string bugs, integer overflow and truncation, type confusion, strict-aliasing violations, data races, TOCTOU, stack and memory exhaustion, and information disclosure.

For each: describe the symptom and root cause, recognise a minimal example, write the corrected version, and know which tool detects it.

Apply defence in depth, secure clearing of sensitive data, safe API design, CERT C / MISRA C awareness and a code-review checklist.

Plain-Language Explanation

C and C++ trust the programmer completely: if you write past the end of an array, the language does not stop you. The program might work today, crash tomorrow, or quietly hand an attacker control. Such mistakes are called undefined behavior (UB) because the language makes no promises at all about what happens next. This module catalogues the bug classes, shows each one being caught by a tool on a tiny, harmless program, and gives the fix. The goal is defence: recognise, fix, prevent.

All demonstrations below are deliberately minimal and safe — they touch a few bytes of the program's own memory and are run under detection tools. Nothing here explains how to exploit a bug.

Precise Technical Explanation

The bug catalogue Fix / prevention Buffer overflow / out-of-bounds index or length not checked corruption, crash later

ASan, _FORTIFY_SOURCE, -Warray-bounds check bounds; spans; bounded APIs overflow of a local array

"stack smashing detected", crash on return stack protector, ASan bounds checks; keep canaries enabled overflow into allocator metadata or neighbours abort inside malloc/free ASan, glibc checks, MALLOC_CHECK_ bounds checks; ownership object used after free/delete wrong data, crash ASan, Valgrind ownership; smart pointers; nulling block freed twice glibc tcache check, ASan single owner; free then NULL lost last reference growing memory use LeakSanitizer, Valgrind, heap profilers RAII; ownership docs; LSan in CI value used before assignment -Wmaybe-uninitialized, MSan, Valgrind initialise at definition missing NULL check SIGSEGV at small address ASan, -fanalyzer, static analysis check results; non-null contracts Dangling pointer/reference pointer outlives object ASan (detect_stack_use_after_return) lifetimes; no escaping locals free of non-heap / interior pointer free only what malloc returned user data used as format crash, data disclosure -Wformat -Wformat-security printf("%s", s) Integer overflow / truncation unchecked arithmetic or narrowing tiny allocations, wrong lengths UBSan, -Wconversion checked arithmetic; wide types Signed overflow in C optimizer removes checks check before; unsigned or wider math Type confusion / unsafe casts object accessed as wrong type UBSan -fsanitize=vptr (C++), review tagged unions; avoid casts punning through incompatible pointers -Wstrict-aliasing (partial) memcpy / bit_cast unsynchronised shared access mutexes, atomics (Module 14) state changes between check and use review, -fanalyzer (some) operate on handles, atomic ops deep recursion, big locals guard pages, -Wstack-usage= bound depth; heap buffers Memory exhaustion DoS unbounded allocation from input OOM kill, swapping load tests, quotas input limits; quotas uninitialised or stale memory sent out zero buffers; calloc

Buffer overflow and out-of-bounds access Writing or reading outside an object's bounds. Writes corrupt neighbouring data; reads leak it. In C nothing checks indexes — but AddressSanitizer surrounds every object with poisoned *redzones* and reports the first bad access (demonstration below).

A stack-buffer overflow that reaches saved registers or the return address. GCC's stack protector places a random canary between the locals and the return address and checks it before returning; a mismatch aborts with * stack smashing detected *.

Overflows or underflows on the heap damage the next block or the allocator's metadata. The crash typically happens *later*, inside an unrelated malloc or free, which makes the cause hard to find without ASan.

Use-after-free, double free and invalid free Covered in Module 6; ASan keeps freed memory in a *quarantine* so later accesses are caught, and glibc detects many double frees immediately. An *invalid free* passes free a pointer it did not return: a stack address, a global, or an interior pointer (free(p + 1)).

Unreachable allocations accumulate. LeakSanitizer scans memory at exit for blocks with no pointers to them (Module 6 shows its report).

Automatic and malloced objects start with indeterminate contents. Reading them is a bug; the compiler may also treat the value as "anything" and optimise surprisingly. GCC warns statically; Clang's MemorySanitizer and Valgrind find it dynamically.

Accessing through a null pointer. On hosted systems the first pages are unmapped so it usually faults immediately — but a large offset from null (p->array[1000000]) may hit mapped memory, and compilers may delete null checks that come after a dereference.

Any pointer whose target's lifetime ended: freed heap blocks, returned locals, iterators into a reallocated vector, string_views of destroyed strings.

printf(user_text) interprets % sequences in the user's text, reading (and with %n, writing) memory the program never intended. Always pass user data as an *argument*: printf("%s", user_text).

Integer overflow and truncation Overflowing size arithmetic or converting a 64-bit length into a 16- or 32-bit variable produces wrong, often small, values that pass later checks. Use size_t for sizes, check before arithmetic, and enable -Wconversion.

Undefined — so if (x + 1 < x) ("did it overflow?") may be compiled to if (0). Check *before* the operation (if (x > INT_MAX - 1)) or use __builtin_add_overflow / C23 ckd_add.

Type confusion and unsafe casts Treating an object as a different type than it is — a wrong downcast, a mis-tagged union, a void * cast to the wrong struct. Prefer tagged unions with checked tags, dynamic_cast or std::variant in C++.

See Module 7: the same program printed 0 at -O0 and 1 at -O2.

Data races and race conditions A data race is two threads accessing the same memory concurrently, at least one writing, without synchronisation — undefined behavior in C/C++. A race condition is the broader logic error where the result depends on timing. ThreadSanitizer finds data races (Module 14).

*Time-of-check to time-of-use*: checking a condition, then acting on it, while something else can change it in between — access(path) then open(path) while an attacker swaps the file for a symlink. Fix by making check and use one atomic operation: open first, then check the opened handle (fstat), use O_NOFOLLOW, openat with directory handles, or O_CREAT | O_EXCL.

Module 5: unbounded recursion or large locals hit the guard page. -Wstack-usage=N and -fstack-usage report per-function stack use at compile time.

Denial of service through memory exhaustion Allocating according to an attacker-supplied size ("the header says 4 GB follow") or keeping unbounded per-client state lets a remote user exhaust memory. Enforce limits before allocating and cap queues and caches.

Information disclosure through uninitialised memory Sending an uninitialised or reused buffer (padding bytes, a partly filled struct, a freed-and-reused block) leaks whatever was there before — keys, passwords, pointers that defeat ASLR. Zero-initialise structs and buffers that leave the process.

Secure allocation and clearing sensitive buffers Clear secrets as soon as they are no longer needed with a function the optimizer may not remove (explicit_bzero, memset_s, SecureZeroMemory, OPENSSL_cleanse), avoid copies, consider mlock to keep pages out of swap, and prefer dedicated secure-memory APIs.

Defense in depth No single measure suffices. Layer: correct code (bounds checks, ownership) → compiler hardening (stack protector, _FORTIFY_SOURCE, CFI) → OS mitigations (ASLR, NX, RELRO, sandboxing) → least privilege (drop root, seccomp) → monitoring. Mitigations make exploitation harder; only correct code removes the bug.

CERT C / MISRA C awareness SEI CERT C — a free, public set of rules and recommendations (e.g. *MEM30-C: Do not access freed memory*, *INT32-C: Ensure that operations on signed integers do not result in overflow*, *STR31-C: Guarantee that storage for strings has sufficient space*).

MISRA C — a proprietary guideline set for safety-related embedded C (automotive, medical, industrial). It restricts language features (dynamic memory, recursion, unions…) to a predictable subset and requires documented deviations. Static analysers check compliance.

This course explains the *ideas* behind these standards; consult the official documents for their exact rules.

Safe API design and defensive programming Design APIs that are hard to misuse: take a pointer *and* a size; return explicit status codes; make ownership obvious in names; validate arguments at trust boundaries; fail closed; use const and opaque types; never return pointers to internal static buffers.

Code review checklist See the Best Practices Checklist below.

Why It Matters in Real Systems

Microsoft and Google have each reported that around 70% of their serious security bugs are memory-safety issues. Memory bugs also cause the hardest-to-reproduce crashes in safety-critical firmware. Recognising them quickly — and running the tools that catch them — is a core professional skill.

Key Terms

Program state for which the language imposes no requirements

Poisoned bytes around objects used by ASan to detect overflows

ASan's delay before reusing freed memory Random value guarding the return address

Compiler instrumentation that checks for bugs at run time Time-of-check to time-of-use race Defense in depth

Multiple independent layers of protection CERT C / MISRA C Secure-coding / safety-coding guideline sets

Memory Layout and Execution Flow

flowchart LR
  B[bug: write past the end] --> R{instrumented build?}
  R -- ASan --> D[redzone hit: report file, line, stack, allocation site]
  R -- plain build --> C[silent corruption]
  C --> L[crash or wrong result later, far away]

Syntax

gcc -g -O1 -fsanitize=address -fno-omit-frame-pointer prog.c      # ASan + LeakSanitizer
gcc -g -fsanitize=undefined prog.c                                  # UBSan
gcc -g -O1 -fsanitize=thread prog.c -pthread                        # TSan (not with ASan)
gcc -O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong prog.c         # hardening
gcc -Wall -Wextra -Wformat=2 -Wconversion -fanalyzer prog.c         # static checks (GCC 10+)

Complete Runnable Example

Each demonstration is a few bytes wide, uses only the program's own memory, and is caught by a tool. ASan reports end with a "shadow bytes" legend; it is cut off here (sed) to keep the output readable.

Heap buffer overflow (off-by-one), caught by AddressSanitizer:

Use after free, caught by AddressSanitizer: Output (one real run):

How to read an ASan report:

First line — the bug class (heap-buffer-overflow, heap-use-after-free) and the bad address.

WRITE/READ of size N and the stack trace — *where* it happened: m09_heap_overflow.c:9, m09_uaf.c:11.

"located 0 bytes to the right of 32-byte region" — the access was just past the end of a 32-byte block (8 ints): an off-by-one.

"freed by thread T0 here" / "previously allocated by" — the free and allocation sites: the full lifetime history of the block.

Double free, caught by glibc without any special build: Stack smashing, caught by the stack protector (on by default in Ubuntu's GCC):

Both are *detections*: the C library notices the corrupted state and deliberately aborts the process before anything worse happens.

Integer overflow, shift and bounds errors, caught by UndefinedBehaviorSanitizer:

With -fno-sanitize-recover, UBSan stops at the first error: the out-of-bounds index on line 15. Without it, it prints every error and continues.

Integer truncation — no sanitizer needed, just arithmetic:

A receiver trusting the 16-bit field allocates 4,464 bytes for a 70,000-byte payload; a negative count becomes an enormous size.

#include <stdlib.h>

int main(void)
{
    size_t n = 8;
    int *values = malloc(n * sizeof *values);
    if (values == NULL) return 1;
    for (size_t i = 0; i <= n; ++i) {     /* bug: <= writes values[8] */
        values[i] = (int)i;
    }
    free(values);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *name = malloc(16);
    if (name == NULL) return 1;
    strcpy(name, "gateway");
    free(name);
    printf("%c\n", name[0]);              /* bug: read after free */
    return 0;
}
for p in m09_heap_overflow m09_uaf; do
  gcc -std=c17 -g -O0 -fsanitize=address -fno-omit-frame-pointer $p.c -o $p
  echo "===== $p ====="
  ./$p 2>&1 | sed '/Shadow bytes around/,$d' | grep -v '^$' || true
done
===== m09_heap_overflow =====
=================================================================
==39==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000060 at pc 0x5a9850e1425b bp 0x7ffd6e168c70 sp 0x7ffd6e168c60
WRITE of size 4 at 0x603000000060 thread T0
    #0 0x5a9850e1425a in main m09_heap_overflow.c:9
    #1 0x7d9aa8e29d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
    #2 0x7d9aa8e29e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f)
    #3 0x5a9850e14104 in _start (m09_heap_overflow+0x1104)
0x603000000060 is located 0 bytes to the right of 32-byte region [0x603000000040,0x603000000060)
allocated by thread T0 here:
    #0 0x7d9aa92b4867 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5a9850e141ec in main m09_heap_overflow.c:6
    #2 0x7d9aa8e29d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: heap-buffer-overflow m09_heap_overflow.c:9 in main
===== m09_uaf =====
=================================================================
==47==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x5806897aa2cf bp 0x7fff5ea425d0 sp 0x7fff5ea425c0
READ of size 1 at
…
#include <stdlib.h>

int main(void)
{
    int *p = malloc(sizeof *p);
    free(p);
    free(p);                              /* bug: second free of the same block */
    return 0;
}
#include <stdio.h>
#include <string.h>

static void fill(char *dst, size_t n) { memset(dst, 'A', n); }

int main(int argc, char *argv[])
{
    (void)argv;
    char small[8];
    size_t n = 8 + 16 * (size_t)argc;     /* 24 at run time: 16 bytes too many */
    fill(small, n);                       /* bug: overruns small and the canary */
    printf("%c\n", small[0]);
    return 0;
}
gcc -std=c17 -O0 m09_double.c -o m09_double
gcc -std=c17 -O0 -fstack-protector-strong m09_smash.c -o m09_smash
echo "===== double free ====="; ./m09_double 2>&1 || echo "exit status $? (128 + 6 = SIGABRT)"
echo "===== stack smashing ====="; ./m09_smash 2>&1 || echo "exit status $? (128 + 6 = SIGABRT)"
===== double free =====
free(): double free detected in tcache 2
m09_glibc.sh: line 3:    64 Aborted                 (core dumped) ./m09_double 2>&1
exit status 134 (128 + 6 = SIGABRT)
===== stack smashing =====
*** stack smashing detected ***: terminated
m09_glibc.sh: line 4:    65 Aborted                 (core dumped) ./m09_smash 2>&1
exit status 134 (128 + 6 = SIGABRT)
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    if (argc < 3) return 1;
    int shift = atoi(argv[1]);            /* 31 */
    int index = atoi(argv[2]);            /* 5 */
    int table[4] = {1, 2, 3, 4};
    int big = INT_MAX;

    unsigned ok = 1u << shift;            /* fine: unsigned, shift < 32 */
    printf("1u << %d = %u\n", shift, ok);
    printf("table[%d] = %d\n", index, table[index]);   /* bug: index out of bounds */
    printf("INT_MAX + 1 = %d\n", big + 1);              /* bug: signed overflow */
    return 0;
}
m09_ubsan.c:15:44: runtime error: index 5 out of bounds for type 'int [4]'
[exit status 1]

Line-by-Line Explanation

i <= n runs nine times; values[8] is the first int *after* the 32-byte block — ASan's redzone catches it on the very first bad write.

After free(name) the block sits in ASan's quarantine, marked poisoned; name[0] reads it and is reported along with where it was freed and allocated.

The double free is caught by glibc's tcache "key" check on the second free.

fill writes 24 bytes into an 8-byte array; on return, main's epilogue finds the canary changed and calls __stack_chk_fail.

UBSan instruments array indexing with known bounds and signed arithmetic and reports the exact line and values.

Casting 70000 to uint16_t keeps 70000 mod 65536 = 4464; casting −1 to size_t gives 2⁶⁴ − 1.

Expected Output

The outputs are shown directly under each demonstration above. ASan, UBSan and glibc messages include process IDs and addresses that vary between runs.

Step-by-Step Execution Trace

For the heap overflow:

malloc(32) returns a block surrounded by poisoned redzone bytes in ASan's shadow memory (1 shadow byte per 8 bytes of memory).

Iterations 0–7 write inside the block; the instrumented store checks the shadow byte each time — all clear.

Iteration 8 writes at values + 32; its shadow byte says "heap left redzone" → ASan prints the report and aborts.

Interactive Visualization

The lifetime visualizer has an "unsafe" example in which an alias outlives its object; the heap simulator shows how freed blocks get reused — which is why a use-after-free can read someone else's data.

Common Mistakes and Failure Modes

Testing only release builds, where the bug corrupts silently instead of being reported.

Disabling a warning instead of fixing the cause.

Checking for overflow *after* the signed operation.

Running ASan and TSan together (they cannot be combined) or forgetting -g (no line numbers).

Fixing the crash site instead of the root cause (the crash in free is rarely where the overflow happened).

Format strings — the compiler warns when user data is used as a format:

#include <stdio.h>

void log_message(const char *user_text)
{
    printf(user_text);            /* bug: user_text is interpreted as a format */
    printf("%s", user_text);      /* correct: user_text is just data */
}
gcc -std=c17 -Wall -Wformat=2 -c m09_format.c -o /dev/null 2>&1 | grep warning | sed 's/^m09_format.c://'
5:5: warning: format not a string literal and no format arguments [-Wformat-security]

Corrected / Safe Pattern

Clearing secrets so the optimizer cannot remove it — the plain memset before returning is a *dead store* and GCC deletes it at -O2; explicit_bzero survives:

with_memset calls use_key and returns — the 32 zero bytes are never written (the %fs:40 lines are the stack-protector canary check). with_explicit_bzero keeps the clearing call; it appears as __explicit_bzero_chk because Ubuntu's GCC enables _FORTIFY_SOURCE, which substitutes a size-checked variant.

for (size_t i = 0; i < n; ++i) values[i] = (int)i;     /* < not <= */

free(name);
name = NULL;                                            /* and do not keep aliases */

if (index < 0 || (size_t)index >= sizeof table / sizeof table[0]) return ERROR;

int sum;
if (__builtin_add_overflow(a, b, &sum)) return ERROR;   /* or C23 ckd_add(&sum, a, b) */

if (payload > UINT16_MAX) return ERROR;                 /* refuse instead of truncating */
cat > wipe.c <<'EOF'
#include <string.h>
void use_key(unsigned char *key);
void with_memset(void)       { unsigned char key[32]; use_key(key); memset(key, 0, sizeof key); }
void with_explicit_bzero(void) { unsigned char key[32]; use_key(key); explicit_bzero(key, sizeof key); }
EOF
gcc -O2 -S -fno-asynchronous-unwind-tables wipe.c -o wipe.s
sed -n '/^with_memset:/,/\.size/p;/^with_explicit_bzero:/,/\.size/p' wipe.s | grep -v '\.size'
with_memset:
	endbr64
	subq	$56, %rsp
	movq	%fs:40, %rax
	movq	%rax, 40(%rsp)
	xorl	%eax, %eax
	movq	%rsp, %rdi
	call	use_key@PLT
	movq	40(%rsp), %rax
	subq	%fs:40, %rax
	jne	.L5
	addq	$56, %rsp
	ret
.L5:
	call	__stack_chk_fail@PLT
with_explicit_bzero:
	endbr64
	pushq	%rbp
	subq	$48, %rsp
	movq	%fs:40, %rax
	movq	%rax, 40(%rsp)
	xorl	%eax, %eax
	movq	%rsp, %rbp
	movq	%rbp, %rdi
	call	use_key@PLT
	movl	$32, %edx
	movl	$32, %esi
	movq	%rbp, %rdi
	call	__explicit_bzero_chk@PLT
	movq	40(%rsp), %rax
	subq	%fs:40, %rax
	jne	.L9
	addq	$48, %rsp
	popq	%rbp
	ret
.L9:
	call	__stack_chk_fail@PLT

Debugging and Diagnostic Techniques

AddressSanitizer (+LeakSanitizer) OOB, UAF, double/invalid free, leaks, stack-use-after-return

GCC/Clang on Linux, macOS (no LSan), Windows (MSVC/Clang) signed overflow, bad shifts, OOB with known bounds, misalignment, null, invalid enum/bool Clang on Linux GCC/Clang, 64-bit Linux/macOS

OOB heap, UAF, leaks, uninitialised use — no recompilation Linux, (macOS limited)

-fanalyzer, Clang Static Analyzer, Coverity, PVS-Studio, CodeQL path-sensitive bugs without running Fuzzers (libFuzzer, AFL++, OSS-Fuzz) inputs that trigger the bugs above

Performance Implications

ASan costs about 2× CPU and 2–3× memory; UBSan is cheaper; TSan 5–15× CPU — use them in testing, not production (except for selected hardened checks). Hardening flags (_FORTIFY_SOURCE, stack protector, CFI) cost a few percent. Bounds checks in hot loops are usually hoisted or removed by optimizers.

Security and Safety Implications

Every class in the catalogue has led to real-world vulnerabilities. Treat any sanitizer report as a must-fix bug, not noise. For new code, prefer memory-safe languages or safe subsets; for existing C/C++, combine coding standards, sanitizers in CI, fuzzing, static analysis and OS mitigations.

Platform and Language Caveats

Sanitizer availability depends on compiler, OS, architecture and build configuration; Windows has ASan (MSVC 16.9+) but not LSan or TSan in MSVC.

explicit_bzero is glibc/BSD; Windows uses SecureZeroMemory; C23 adds memset_explicit.

Java, C#, Python, JavaScript and safe Rust eliminate out-of-bounds writes, use-after-free and double free by construction; they still have integer, logic, TOCTOU, injection, resource-exhaustion and (except Rust) data-race issues.

Real-World Application

OSS-Fuzz has found tens of thousands of bugs in open-source C/C++ projects using ASan, UBSan and MSan. Chromium, Android and the Linux kernel run sanitizers continuously. Automotive suppliers check MISRA compliance on every commit.

Best Practices Checklist

[ ] Every index and length from input is checked before use.

[ ] Every allocation is checked, owned, freed once.

[ ] No pointer outlives its object; no escaping addresses of locals.

[ ] Every variable is initialised.

[ ] Integer arithmetic on sizes is overflow-checked; no silent narrowing.

[ ] User data is never a format string.

[ ] Shared data is synchronised.

[ ] Secrets are wiped with non-optimisable functions.

[ ] CI runs ASan/UBSan (and TSan for threaded code) and a fuzzer on parsers.

[ ] Hardening flags and OS mitigations are enabled in release builds.

Exercises

E1 · Beginner. Name the bug class: char b[4]; strcpy(b, "four");

Answer: Stack buffer overflow — "four" needs 5 bytes including the terminator.

E2 · Intermediate. Why can if (len + 100 < len) return ERROR; be deleted by the compiler when len is an int, but not when it is size_t?

Answer: Signed overflow is undefined, so the compiler may assume len + 100 < len is always false. Unsigned arithmetic wraps by definition, so the check is meaningful (though if (len > SIZE_MAX - 100) is clearer).

E3 · Advanced challenge. Write a function int copy_record(uint8_t *dst, size_t dst_size, const uint8_t *src, size_t src_size) that copies a record whose first two bytes are a big-endian length, and that cannot overflow either buffer for any input.

Answer: Check src_size >= 2; read len = (src[0] << 8) | src[1]; check len <= src_size - 2 and len <= dst_size; then memcpy(dst, src + 2, len) and return len; return −1 on any failed check. All subtractions happen only after the comparisons that make them non-negative.

Quiz

? ASan says "heap-buffer-overflow ... 0 bytes to the right of 32-byte region". What happened?
+ The program accessed the byte just past the end of a 32-byte heap block. :: A classic off-by-one.
- The heap ran out of memory. :: That would be an allocation failure, not an overflow report.
- A 32-byte block was freed twice. :: That is reported as double-free.
- The stack overflowed by 32 bytes. :: The region is on the heap.

? Which fix prevents format-string vulnerabilities?
- printf(user_text) with -O2 :: Optimization changes nothing.
+ printf("%s", user_text) :: User text is passed as data, not as a format.
- Escaping % characters manually in every caller :: Fragile; the correct API call is simpler.
- puts(user_text) is also unsafe :: puts does not interpret format sequences, so it is safe too.

? Which tools detect data races? (select all that apply)
+ ThreadSanitizer :: It instruments memory accesses and synchronisation.
- AddressSanitizer :: It detects memory errors, not races.
+ Helgrind / DRD (Valgrind tools) :: They detect races and lock misuse dynamically.
- -Wall :: Races are not visible to simple warnings.

? Why does memset(key, 0, sizeof key) right before a function retu
…

Summary

Undefined behavior means the language promises nothing; memory bugs are the dominant source of crashes and vulnerabilities in C and C++. Each class has a recognisable cause, a tool that detects it — ASan, UBSan, TSan, glibc's checks, the stack protector, compiler warnings, static analysis and fuzzers — and a straightforward fix. Real safety comes from correct code backed by tools and layered mitigations.

References and Further Reading

SEI CERT C Coding Standard (wiki.sei.cmu.edu) — free online.

MISRA C:2012 / MISRA C:2023 guidelines (overview at misra.org.uk).

Google Sanitizers wiki (github.com/google/sanitizers).

Microsoft Security Response Center, "A proactive approach to more secure code" (2019); Chromium "Memory safety" page.

CWE Top 25 Most Dangerous Software Weaknesses (MITRE).