L11 · Memory-Safe Languages, Safer C, and the Secure Software Process

CS 161, Lesson 11, in 50 slides and code mode. It lays out the spectrum of memory-safety fixes: memory-safe languages, which are the only 100% guarantee; Rust's ownership and borrow model, which achieves that without a garbage collector; safer C through pre- and post-conditions and bounds-checked library calls, replacing gets with fgets, strcpy with strncpy or strlcpy, and sprintf with snprintf; and the secure-development process, covering controlled crashes, code review, fuzzing, Valgrind, sanitizers, static analyzers, and the SDL. It is anchored to textbook sections 4.1 to 4.3.

Subject: Computer Security · 85 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. The Right Fix

Title

CS 161 · Lesson 11 of 45

memory-safe languages · Rust ownership · safer C libraries · fuzzing · Valgrind · the secure-development process

2. By the end of this lesson you can…

Objectives

  1. Explain why a memory-safe language is the ONLY way to stop 100% of memory-safety vulnerabilities, and why C persists anyway.
  2. Describe Rust's ownership / borrow model at a high level — memory-safe with no garbage collector.
  3. Write safer C by swapping unbounded calls for bounded ones: gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf.
  4. Name the tools and process that find remaining bugs: controlled crashes, code review, fuzzing, Valgrind, sanitizers, static analyzers.
  5. Place every defense on the spectrum from strongest guarantee (the language) to weakest (finds-some-bugs).

3. What survived from L10 · Heap Bugs: Use-After-Free, Double-Free, vtable & Type…?

Warm-up

Discussion prompt

Before we open L11 · Memory-Safe Languages, Safer C, and the Secure Software Process: without looking back, what was the main idea of L10 · Heap Bugs: Use-After-Free, Double-Free, vtable & Type Confusion, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

CS 161 Lesson 10 (50 slides, code mode): dangling pointers and use-after-free, the two-objects-share-one-block exploitation primitive, double-free corrupting the allocator free-list, the C++ vtable-pointer overwrite (a pointer-to-a-pointer hijack), supplemental type confusion, and why stack canaries miss the heap. Toy/sandbox examples only.

4. Where this lesson sits

Concept

Lessons 8–10 showed how memory bugs in C become exploits. This lesson is the other half: how to actually stop them. The whole unit has been pointing here.

The thesis in one line: only a memory-safe language gives a 100% guarantee. Everything else on the spectrum reduces or finds bugs — it does not eliminate the class.

5. Break it if you can: Where this lesson sits

Counterexample

Discussion prompt

Lessons 8–10 showed how memory bugs in C become exploits. This lesson is the other half: how to actually stop them. The whole unit has been pointing here.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

The thesis in one line: only a memory-safe language gives a 100% guarantee. Everything else on the spectrum reduces or finds bugs — it does not eliminate the class.

6. Use a Memory-Safe Language

Section

Part 1 · §4.1

7. The one guarantee that actually holds

Concept

Java, Python, Go, Rust, and Swift combine compile-time and runtime checks that prevent memory errors no matter what the programmer does. An out-of-bounds access can't silently corrupt memory — it's caught.

memory-safe language — A language whose compile-time and runtime checks make memory-safety violations impossible regardless of programmer mistakes.

§4.1's exact thesis: using a memory-safe language is the ONLY way to stop 100% of memory safety vulnerabilities.

8. So why is anyone still writing C?

Concept

Two reasons, and only two: legacy code (decades of C in kernels, libraries, and firmware that nobody will rewrite) and perceived performance concerns.

Note the word perceived. The performance gap is mostly a story we tell ourselves — and as the next slide shows, the one real piece of it has already been closed.

Reason C persistsReal?
Legacy code already written in Cyes — install base never shrinks
Perceived performance needmostly perception
Memory safety requires giving up speedfalse — see Rust

9. Fill in: Real? for So why is anyone still writing C?

Comparison

Comparison matrix

From So why is anyone still writing C?: refill the Real? column from what you know. The rest of the table is as it appeared.

Reason C persistsReal?
Legacy code already written in Cyes — install base never shrinks
Perceived performance needmostly perception
Memory safety requires giving up speedfalse — see Rust

10. The performance argument, fairly stated

Intuition

Be fair to C. The book's footnote concedes one genuine advantage: C has more deterministic memory-allocation behavior than a garbage-collected language like Go — no GC pause arrives at an unpredictable moment.

That matters for hard-real-time and tight-latency code. So the performance worry isn't pure myth — it has exactly one true core.

But here's the catch the next slide makes precise: Rust is memory-safe without a garbage collector — so even that one advantage is gone.

11. Rust: safe AND no garbage collector ⊕

Concept

⊕ Beyond the book's depth, but it dissolves the whole debate. Rust achieves memory safety with NO garbage collector. It keeps C's deterministic allocation and C's speed and full memory safety — all three.

How? Not with a runtime collector but with the ownership model, enforced entirely at compile time by the borrow checker. The cost is paid by the programmer and the compiler, not by a runtime GC.

Consequence: the 'C is faster because no GC' argument no longer points at C — it points at Rust.

12. By analogy: Rust: safe AND no garbage collector ⊕

Analogy

Discussion prompt

Explain Rust: safe AND no garbage collector ⊕ by analogy to something with no Computer Security in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Consequence: the 'C is faster because no GC' argument no longer points at C — it points at Rust.

13. Ownership in plain language ⊕

Intuition

⊕ Think of every value as having exactly one owner. When the owner goes out of scope, the value is freed — automatically, predictably, at compile-time-known points. No GC needed to decide when.

You can borrow a value (take a reference) but the borrow checker enforces two rules at compile time: you can't use a value after it's freed, and you can't have a mutable alias while anything else is reading it.

Those two rules are exactly what kills use-after-free and buffer overflows — the bugs that filled Lessons 8–10 simply don't compile in safe Rust.

14. The borrow checker, concretely ⊕

Concept

⊕ A use-after-free in C is a runtime time bomb. In Rust the same shape is a compile error — the program never ships.

let r;
{
    let x = 5;
    r = &x;          // borrow x
}                     // x is dropped (freed) here
println!("{}", r);   // ERROR: `x` does not live long enough
Memory bug classCSafe Rust
use-after-freesilent UB at runtimecompile error (lifetime)
buffer overflowsilent corruptionbounds-checked / rejected
mutable aliasingdata race / corruptioncompile error (borrow rules)

15. What each one costs: The borrow checker, concretely ⊕

Trade off

Comparison matrix

From The borrow checker, concretely ⊕: every row here is a choice with a cost. Fill the C column, then say which row you would actually pick and what you give up for it.

Memory bug classCSafe Rust
use-after-freesilent UB at runtimecompile error (lifetime)
buffer overflowsilent corruptionbounds-checked / rejected
mutable aliasingdata race / corruptioncompile error (borrow rules)

16. Runtime checks vs. compile-time checks

Intuition

Memory-safe languages split the work two ways. Compile-time checks reject unsafe code before it ever runs; runtime checks (like an array-bounds test on each access) catch what can only be known while running.

Java and Python lean heavily on runtime checks (and a GC). Rust pushes almost everything to compile time via ownership — which is why it pays no runtime GC cost.

LanguageMain mechanismRuntime GC?
Java / Pythonruntime checks + GCyes
Goruntime checks + GCyes
Rustcompile-time ownershipno

17. Teach it back: Runtime checks vs. compile-time checks

Explain it

Discussion prompt

Explain Runtime checks vs. compile-time checks to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Java and Python lean heavily on runtime checks (and a GC). Rust pushes almost everything to compile time via ownership — which is why it pays no runtime GC cost.

18. Guess the shape of the answer: The same bug across three worlds

Estimation

Predict first

Take one classic bug — index past the end of an array — and watch what each world does with it.

Commit before you compute: what does The same bug across three worlds come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Python raises, Rust panics — both FAIL SAFE

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. A raised exception or a controlled panic is an availability loss, never a memory-corruption takeover.

19. The same bug across three worlds

Worked example

Take one classic bug — index past the end of an array — and watch what each world does with it.

// C:      buf[10] on a 8-element buffer
//   -> silent out-of-bounds write, possible takeover
// Python: lst[10] on an 8-element list
//   -> IndexError raised at runtime, no corruption
// Rust:   v[10] on an 8-element Vec
//   -> panic (controlled), or compile error if statically known

C does nothing — the write lands in adjacent memory

Why: No bounds check exists; this is exactly the L8 overflow.

Python raises, Rust panics — both FAIL SAFE

Why: A raised exception or a controlled panic is an availability loss, never a memory-corruption takeover.

WorldOut-of-bounds indexOutcome
Cbuf[10]silent corruption — exploitable
Pythonlst[10]IndexError — safe failure
Rustv[10]panic / compile error — safe failure

20. Inspect it line by line: The same bug across three worlds

Error analysis

Annotate

Walk the callouts on The same bug across three worlds. Each one is a place this is easy to get subtly wrong.

  • No bounds check exists; this is exactly the L8 overflow.
  • A raised exception or a controlled panic is an availability loss, never a memory-corruption takeover.

21. Something is wrong here: 'Rust is safe because it has a garbage collector'

Anomaly

Predict first

A student writes this, and it looks reasonable:

How does Rust avoid use-after-free and overflows?

It is wrong. Say what breaks — and say it before you turn the page.

Correct: FALSE. If that were true, Rust would inherit the GC pause and lose C's deterministic allocation — and there'd be no reason to prefer it over Go.

How does Rust avoid use-after-free and overflows?

Why: FALSE. If that were true, Rust would inherit the GC pause and lose C's deterministic allocation — and there'd be no reason to prefer it over Go.

22. Trap: 'Rust is safe because it has a garbage collector'

Trap

The trap

How does Rust avoid use-after-free and overflows?

Assume Rust must run a garbage collector like Java or Go

Why: FALSE. If that were true, Rust would inherit the GC pause and lose C's deterministic allocation — and there'd be no reason to prefer it over Go.

The fix

How does Rust avoid use-after-free and overflows?

Ownership + the borrow checker, enforced at COMPILE time — no GC

Why: §4.1 footnote ⊕: Rust is memory-safe with no garbage collector. That is exactly why it erases C's last performance advantage (deterministic allocation) while staying safe.

23. Writing Memory-Safe Code in C

Section

Part 2 · §4.2

24. When you're stuck in C anyway

Intuition

Sometimes you can't switch languages — you're patching a 20-year-old C daemon. §4.2 gives two approaches for writing memory-safe C: reason about every access, or lean on safe libraries.

Neither is a 100% guarantee like the language was. Both reduce risk and both depend on programmer discipline — keep that ranking in mind.

25. Approach (a): pre/post-conditions & invariants

Concept

invariant — A condition the code guarantees is always true at a given point — e.g. 'i is always < len here' — used to prove each memory access is in bounds.

You annotate each function with a pre-condition (what must hold on entry) and post-condition (what holds on exit), then reason that every access is safe. A genuinely good skill — but the book calls it painstakingly tedious and rarely used, and explicitly out of scope for this class.

26. Defensive programming

Intuition

defensive programming — Adding checks 'just in case' — e.g. always verifying a pointer is non-NULL before dereferencing it, even when you're sure it's valid.

The idea is to never trust your own assumptions: check the bound, check the pointer, check the length — every time. It helps, but it relies on programmer discipline and is tedious. Miss one check and the gap reopens.

27. Take the definitions apart: invariant vs defensive programming

Definition probe

Sort into buckets

Every line below is part of the definition of invariant or of defensive programming — one or the other, never both. Put each where it belongs.

invariant
A condition the code guarantees is always true at a given point; used to prove each memory access is in bounds.
defensive programming
Adding checks 'just in case'; always verifying a pointer is non-NULL before dereferencing it, even when you're sure it's valid.
b1
A condition the code guarantees is always true at a given point — e.g. 'i is always < len here' — used to prove each memory access is in bounds.
b2
Adding checks 'just in case' — e.g. always verifying a pointer is non-NULL before dereferencing it, even when you're sure it's valid.

28. Approach (b): safe libraries that bounds-check for you

Concept

The practical workhorse: replace each unbounded standard-library call with a bounded twin that takes a size and refuses to write past it. The library does the counting so you can't forget.

§4.2's exact replacements: fgets for gets; strncpy or strlcpy for strcpy; snprintf for sprintf.

This is a reduction, not a guarantee — but it's the highest-leverage move available inside C.

29. What has to be given first: Dangerous call vs. safe replacement, side by…

Missing information

Discussion prompt

Each dangerous call below has a size-aware twin. Read the unsafe line, then the safe line, then what the safe version actually checks.

What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.

Hint: Anything you would have to invent to get started is a thing the problem must supply.

Answer:

Every bounded twin gains one parameter: the destination size. That single number is what makes the overflow impossible.

30. Dangerous call vs. safe replacement, side by side

Worked example

Each dangerous call below has a size-aware twin. Read the unsafe line, then the safe line, then what the safe version actually checks.

gets(buf);                          // unbounded — reads until newline/EOF
fgets(buf, sizeof(buf), stdin);     // bounded — at most sizeof(buf)-1 bytes

strcpy(dst, src);                   // unbounded — copies until src's '\0'
strncpy(dst, src, sizeof(dst));     // bounded — copies at most sizeof(dst)
strlcpy(dst, src, sizeof(dst));     // bounded — and always null-terminates

sprintf(buf, "%s", s);              // unbounded — formats with no limit
snprintf(buf, sizeof(buf), "%s", s);// bounded — writes at most size-1 + '\0'

Read off the size argument in each safe form

Why: Every bounded twin gains one parameter: the destination size. That single number is what makes the overflow impossible.

Match each unsafe API to its safe replacement and what it checks

Why: This is the table you reach for in code review — the 'unsafe → safe → what it checks' lookup below.

Unsafe APISafe replacementWhat it checks
gets(buf)fgets(buf, sizeof(buf), stdin)reads at most size-1 bytes, then '\0'
strcpy(dst, src)strncpy / strlcpy (dst, src, sizeof(dst))bounded copy — stops at the destination size
sprintf(buf, ...)snprintf(buf, sizeof(buf), ...)bounded format — at most size-1 chars written

31. Fill in: Safe replacement for Dangerous call vs. safe replacement, side by…

Comparison

Comparison matrix

From Dangerous call vs. safe replacement, side by side: refill the Safe replacement column from what you know. The rest of the table is as it appeared.

Unsafe APISafe replacementWhat it checks
gets(buf)fgets(buf, sizeof(buf), stdin)reads at most size-1 bytes, then '\0'
strcpy(dst, src)strncpy / strlcpy (dst, src, sizeof(dst))bounded copy — stops at the destination size
sprintf(buf, ...)snprintf(buf, sizeof(buf), ...)bounded format — at most size-1 chars written

32. Something is wrong here: assuming strncpy always null-terminates

Anomaly

Predict first

A student writes this, and it looks reasonable:

Using strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: It does NOT. When the source length equals the buffer size, strncpy fills every byte with source data and writes no terminator — callback to L9, the next strlen/printf then runs off the end.

Using strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.

Why: It does NOT. When the source length equals the buffer size, strncpy fills every byte with source data and writes no terminator — callback to L9, the next strlen/printf then runs off the end.

33. Trap: assuming strncpy always null-terminates

Trap

The trap

Using strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.

Assume strncpy always leaves a '\0' at the end

Why: It does NOT. When the source length equals the buffer size, strncpy fills every byte with source data and writes no terminator — callback to L9, the next strlen/printf then runs off the end.

The fix

Using strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.

Manually terminate, or use strlcpy which always terminates

Why: §4.2: after strncpy, set dst[sizeof(dst)-1] = '\0'; or use strlcpy, which guarantees a terminator. 'Bounded' did not mean 'null-terminated' — those are two separate properties.

34. Why the library, not your own counting

Intuition

You could hand-write the bounds check before every copy. But humans miscount, copy-paste a len-1, or forget the check entirely on the fifth call site. The whole value of a safe library is that the counting moves inside the function, where you can't skip it.

One parameter — the destination size — carries all the safety. Pass it correctly once per call and the library handles the rest, every time.

35. Guess the shape of the answer: Wiring fgets into the L8 vulnerable()…

Estimation

Predict first

Bring back L8's vulnerable() and apply approach (b) directly — the bounded twin closes the exact hole we spent three lessons attacking.

Commit before you compute: what does Wiring fgets into the L8 vulnerable() function come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Use sizeof(buf), never a literal 8

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. If buf's size changes later, sizeof tracks it; a hard-coded 8 would silently drift and reintroduce the overflow.

36. Wiring fgets into the L8 vulnerable() function

Worked example

Bring back L8's vulnerable() and apply approach (b) directly — the bounded twin closes the exact hole we spent three lessons attacking.

void vulnerable() {
    char buf[8];
    // gets(buf);                       // L8: unbounded -> overflow
    fgets(buf, sizeof(buf), stdin);     // bounded: <= 7 bytes + '\0'
}

Swap gets for fgets with sizeof(buf)

Why: fgets is told the buffer size, so it stops at 7 input bytes and writes the terminator — it can never reach the sfp or rip above buf.

Use sizeof(buf), never a literal 8

Why: If buf's size changes later, sizeof tracks it; a hard-coded 8 would silently drift and reintroduce the overflow.

CallMax bytes writtenReaches rip?
gets(buf)unboundedyes — hijack
fgets(buf, sizeof(buf), stdin)7 + '\0' = 8no — capped at buf
fgets(buf, 8, stdin) // literal8no, but fragile if buf grows

37. Inspect it line by line: Wiring fgets into the L8 vulnerable()…

Error analysis

Annotate

Walk the callouts on Wiring fgets into the L8 vulnerable() function. Each one is a place this is easy to get subtly wrong.

  • fgets is told the buffer size, so it stops at 7 input bytes and writes the terminator — it can never reach the sfp or rip above buf.
  • If buf's size changes later, sizeof tracks it; a hard-coded 8 would silently drift and reintroduce the overflow.

38. Why approach (b) still isn't 100%

Concept

Safe libraries only protect the call sites you remember to convert. Miss one strcpy in 50,000 lines and the overflow is back. The guarantee is per-call, not per-program.

Contrast the language guarantee from Part 1: there, safety held no matter what the programmer does. Here it holds only where the programmer was careful — that's the whole difference on the spectrum.

39. Building Secure Software

Section

Part 3 · §4.3

40. Assume bugs remain — then catch them

Intuition

If you're in C and can't prove the code safe, the realistic stance is: bugs remain. §4.3 is the toolbox for finding them before attackers do, and for failing safely when one slips through.

Notice the demotion: Part 1 eliminated the bug class, Part 2 reduced it, Part 3 only finds some of what's left. Useful, never sufficient.

41. Run-time checks → a controlled crash

Concept

Add automatic run-time bounds checks, and when one fails, direct the program to a controlled crash instead of continuing. A crash is a denial-of-service; a successful overflow is a takeover — the crash is the far better outcome.

controlled crash — Deliberately halting the program the instant a check fails, so the attacker gets a crash (availability loss) instead of code execution (full compromise).

The goal isn't to keep running — it's to make sure the attacker doesn't succeed.

42. Code review by a human

Concept

A second person reading the code catches bugs that tools and the author miss — especially logic and design flaws. It is expensive (human time) but valuable.

No automation fully replaces it; the most security-critical changes get human eyes precisely because the cost is justified there.

43. Where does each piece belong: L11 · Memory-Safe Languages, Safer C, and…

Sorting

Sort into buckets

These are the pieces of L11 · Memory-Safe Languages, Safer C, and the Secure Software Process, out of order. Put each one back under the part of the lesson it belongs to.

Use a Memory-Safe Language
The one guarantee that actually holds; So why is anyone still writing C?; The performance argument, fairly stated
Writing Memory-Safe Code in C
When you're stuck in C anyway; Approach (a): pre/post-conditions & invariants; Defensive programming
Building Secure Software
Assume bugs remain — then catch them; Run-time checks → a controlled crash; Code review by a human
s1
Use a Memory-Safe Language is where L11 · Memory-Safe Languages, Safer C, and the Secure Software Process puts The one guarantee that actually holds, So why is anyone still writing C?, The performance argument, fairly stated. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
Writing Memory-Safe Code in C is where L11 · Memory-Safe Languages, Safer C, and the Secure Software Process puts When you're stuck in C anyway, Approach (a): pre/post-conditions & invariants, Defensive programming. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Building Secure Software is where L11 · Memory-Safe Languages, Safer C, and the Secure Software Process puts Assume bugs remain — then catch them, Run-time checks → a controlled crash, Code review by a human. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

44. Fuzz testing

Concept

fuzz testing — Testing a program with large amounts of random inputs and corner cases, watching for crashes or memory errors that reveal bugs.

Throw random and malformed inputs at the program and see what breaks. Cheap, automatable, and shockingly effective at surfacing the inputs a human tester would never think to type.

Crucial limit: a fuzzer finds bugs it happens to trigger — it can never prove their absence (more on this in the misconceptions slide).

45. Term to definition: L11 · Memory-Safe Languages, Safer C, and the Secure Software Process

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. memory-safe language
  • t2. invariant
  • t3. defensive programming
  • t4. controlled crash
  • t5. fuzz testing
  • d1. A language whose compile-time and runtime checks make memory-safety violations impossible regardless of programmer mistakes.
  • d2. A condition the code guarantees is always true at a given point — e.g. 'i is always < len here' — used to prove each memory access is in bounds.
  • d3. Adding checks 'just in case' — e.g. always verifying a pointer is non-NULL before dereferencing it, even when you're sure it's valid.
  • d4. Deliberately halting the program the instant a check fails, so the attacker gets a crash (availability loss) instead of code execution (full compromise).
  • d5. Testing a program with large amounts of random inputs and corner cases, watching for crashes or memory errors that reveal bugs.

Why: These are the working definitions of memory-safe language, invariant, defensive programming, controlled crash, fuzz testing as L11 · Memory-Safe Languages, Safer C, and the Secure Software Process uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.

46. What has to happen first: What a fuzzer is doing, conceptually

Ranking

Put in order

Put the moves of What a fuzzer is doing, conceptually into the order they have to happen.

  1. Generate inputs the author never tested
  2. A saved crash means a bug EXISTS, here
  3. But no crash does NOT mean 'safe'

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Random mutation explores corner cases — empty strings, giant strings, embedded nulls — exactly where memory bugs hide.

47. What a fuzzer is doing, conceptually

Worked example

A fuzzer loops: generate a weird input, run the target, check whether it crashed or tripped a sanitizer. Each crash is a candidate bug to triage.

while True:
    data = mutate(seed)        # random / corner-case input
    result = run(target, data) # feed it to the program
    if result.crashed:
        save_crashing_input(data)  # a bug to investigate

Generate inputs the author never tested

Why: Random mutation explores corner cases — empty strings, giant strings, embedded nulls — exactly where memory bugs hide.

A saved crash means a bug EXISTS, here

Why: Each crashing input is concrete proof of one bug.

But no crash does NOT mean 'safe'

Why: The loop only covers inputs it actually tried. Untried inputs may still crash — absence of evidence is not evidence of absence.

Fuzzer outcomeWhat it proves
crash founda bug exists (a real one)
no crash yetnothing — just not found yet
ran 10^9 inputs cleanhigh confidence, NOT a proof

48. Where the cost goes: What a fuzzer is doing, conceptually

Cost model

Annotate

In What a fuzzer is doing, conceptually, before reading the notes: mark where the time actually goes. Which line dominates?

  • Random mutation explores corner cases — empty strings, giant strings, embedded nulls — exactly where memory bugs hide.
  • Each crashing input is concrete proof of one bug.
  • The loop only covers inputs it actually tried. Untried inputs may still crash — absence of evidence is not evidence of absence.

49. Why random inputs beat careful ones

Intuition

A human tester writes inputs they expect — names, dates, sensible numbers. Attackers send the inputs you'd never expect: a 10-megabyte username, a string of all nulls, deeply nested brackets. Randomness covers that space cheaply.

The fuzzer doesn't need to be smart, just relentless. Millions of weird inputs per minute surface the corner cases a person would take years to enumerate.

50. Coverage-guided fuzzing ⊕

Concept

⊕ Modern fuzzers (AFL, libFuzzer) are coverage-guided: they keep inputs that reach new code paths and mutate those further, steering the randomness toward unexplored branches instead of flailing blindly.

Paired with ASan, a coverage-guided fuzzer turns a silent overflow into an immediate, reproducible crash report — the single highest-yield bug-finding setup for C code today.

Fuzzer styleStrategyEffectiveness
dumb / black-boxpure random mutationfinds shallow bugs
coverage-guidedkeep inputs hitting new pathsreaches deep bugs
coverage-guided + ASan+ detect silent corruptionbest for memory bugs

51. What each one costs: Coverage-guided fuzzing ⊕

Trade off

Comparison matrix

From Coverage-guided fuzzing ⊕: every row here is a choice with a cost. Fill the Strategy column, then say which row you would actually pick and what you give up for it.

Fuzzer styleStrategyEffectiveness
dumb / black-boxpure random mutationfinds shallow bugs
coverage-guidedkeep inputs hitting new pathsreaches deep bugs
coverage-guided + ASan+ detect silent corruptionbest for memory bugs

52. Valgrind and code-coverage tools

Concept

Valgrind instruments a running program to detect memory leaks and invalid accesses you'd otherwise never see. Run your tests under it and the silent corruption becomes a loud report.

code coverage — A measure of how much of the program's code your tests actually exercise — used to gauge 'have I tested enough?'

Coverage tools help answer 'have I tested enough?' — but it's genuinely hard to know: 100% line coverage still misses input-dependent and path-dependent bugs.

53. Sanitizers & static analyzers ⊕

Concept

⊕ Beyond the book: AddressSanitizer (ASan) and MemorySanitizer (MSan) are compile-time instrumentation — recompile with a flag and the binary self-detects overflows, use-after-free, and uninitialized reads at runtime, ideal paired with a fuzzer.

static analyzer — A tool that inspects source code WITHOUT running it, flagging suspicious patterns — e.g. CodeQL, Coverity, clang-analyzer.

⊕ Static analyzers (CodeQL, Coverity, clang-analyzer) find bugs without executing the program — but they over-report (false positives) and miss things (false negatives). A tool, not an oracle.

54. The Secure Development Lifecycle ⊕

Concept

⊕ Beyond the book: mature shops wrap all of the above in a process — Microsoft's Secure Development Lifecycle (SDL) is the canonical example. Security isn't a final step; it's a gate at every stage.

55. Something is wrong here: 'fuzzing passed, so the program is safe'

Anomaly

Predict first

A student writes this, and it looks reasonable:

A fuzzer ran a billion inputs and found nothing.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: A fuzzer only explores inputs it generated.

A fuzzer ran a billion inputs and found nothing.

Why: A fuzzer only explores inputs it generated. 'No crash found' means 'not found yet' — the very next untried input could still overflow. Testing can show presence of bugs, never their absence.

56. Trap: 'fuzzing passed, so the program is safe'

Trap

The trap

A fuzzer ran a billion inputs and found nothing.

Conclude the program is now proven memory-safe

Why: A fuzzer only explores inputs it generated. 'No crash found' means 'not found yet' — the very next untried input could still overflow. Testing can show presence of bugs, never their absence.

The fix

A fuzzer ran a billion inputs and found nothing.

Treat it as evidence of effort, not a guarantee

Why: §4.3: fuzzing FINDS bugs; only a memory-safe language ELIMINATES the class. Clean fuzzing raises confidence — it does not move you to the 100% column.

57. Break it on purpose: 'fuzzing passed, so the program is safe'

Break the constraint

Discussion prompt

The rule this trap just fixed:

A fuzzer ran a billion inputs and found nothing.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

A fuzzer only explores inputs it generated. 'No crash found' means 'not found yet' — the very next untried input could still overflow. Testing can show presence of bugs, never their absence.

58. Something is wrong here: 'we switched to safe libraries, so C is fully safe now'

Anomaly

Predict first

A student writes this, and it looks reasonable:

The team replaced every gets they could find with fgets.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Safe libraries protect only the call sites you converted.

The team replaced every gets they could find with fgets.

Why: Safe libraries protect only the call sites you converted. One missed strcpy, one new sprintf added next sprint, and the overflow is back. It's a reduction, not a guarantee.

59. Trap: 'we switched to safe libraries, so C is fully safe now'

Trap

The trap

The team replaced every gets they could find with fgets.

Declare the codebase memory-safe

Why: Safe libraries protect only the call sites you converted. One missed strcpy, one new sprintf added next sprint, and the overflow is back. It's a reduction, not a guarantee.

The fix

The team replaced every gets they could find with fgets.

Call it 'risk reduced', keep the tools running, plan for a safe language

Why: §4.2/§4.3: bounded APIs + fuzzing + review reduce and find bugs. The only 100% fix remains a memory-safe language; the tools buy time on legacy C.

60. Which of these survive contact with L11 · Memory-Safe Languages, Safer C, and…?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
Lessons 8–10 showed how memory bugs in C become exploits. This lesson is the other half: how to actually stop them. The whole unit has been pointing here.; Two reasons, and only two: legacy code (decades of C in kernels, libraries, and firmware that nobody will rewrite) and perceived performance concerns.; Consequence: the 'C is faster because no GC' argument no longer points at C — it points at Rust.
Breaks
How does Rust avoid use-after-free and overflows?; Using strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.
sound
These are stated as this lesson states them — each one survives the edge cases L11 · Memory-Safe Languages, Safer C, and the Secure Software Process puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

61. The Spectrum of Fixes

Section

Part 4 · Synthesis

62. Strongest to weakest guarantee

Concept

Line everything up by how strong the guarantee is, not by how new or clever it is. There's a clear ordering, and only the first rung is a 100% guarantee.

  1. Memory-safe LANGUAGE — eliminates the bug class. The only 100% guarantee.
  2. Safe LIBRARIES + defensive code — reduces it, one call site at a time.
  3. TOOLS / fuzzing / review — finds some of what remains.
  4. MITIGATIONS (L12–L15) — contain what's still left at runtime.

63. Why the ordering is the whole point

Intuition

It's tempting to treat fuzzing, Valgrind, and fgets as 'security done'. They're not interchangeable with the language fix — they sit lower on the same ladder, each catching less.

A mature team uses all of them, but never confuses 'we fuzz and review' with 'we eliminated overflows'. Only the top rung does that.

64. Teach it back: Why the ordering is the whole point

Explain it

Discussion prompt

Explain Why the ordering is the whole point to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

A mature team uses all of them, but never confuses 'we fuzz and review' with 'we eliminated overflows'. Only the top rung does that.

65. What has to be given first: Reading the spectrum as a table

Missing information

Discussion prompt

Same four rungs, now with what each one actually delivers — this is the slide to internalize for the check.

What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.

Hint: Anything you would have to invent to get started is a thing the problem must supply.

Answer:

Eliminating the class means no instance can exist — that's qualitatively different from reducing, finding, or containing instances.

66. Reading the spectrum as a table

Worked example

Same four rungs, now with what each one actually delivers — this is the slide to internalize for the check.

STRONGEST  memory-safe language   -> eliminates the bug class   (100%)
   |       safe libraries + defense -> reduces risk per call site
   |       tools / fuzz / review    -> finds some remaining bugs
WEAKEST    mitigations (L12-L15)    -> contains what survives at runtime

Only the top rung is a guarantee

Why: Eliminating the class means no instance can exist — that's qualitatively different from reducing, finding, or containing instances.

Each lower rung exists BECAUSE C will persist

Why: If everyone rewrote in Rust tomorrow, rungs 2–4 would be unnecessary for that code. They're the price of legacy C.

RungVerb100%?
memory-safe languageeliminatesyes
safe libraries + defensive codereducesno
tools / fuzzing / reviewfindsno
runtime mitigations (next unit)containsno

67. Which is which, by 100%?

Discrimination

Sort into buckets

Sort these by 100%?, from memory, without looking back at Reading the spectrum as a table. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.

yes
memory-safe language
no
safe libraries + defensive code; tools / fuzzing / review; runtime mitigations (next unit)
g1
100%? is "yes" for memory-safe language — that is what the table on "Reading the spectrum as a table" records, and it is the single property separating this group from the rest.
g2
100%? is "no" for safe libraries + defensive code, tools / fuzzing / review, runtime mitigations (next unit) — that is what the table on "Reading the spectrum as a table" records, and it is the single property separating this group from the rest.

68. Guess the shape of the answer: Placing one real CVE on the spectrum

Estimation

Predict first

A team ships a C image parser. A memcpy with an attacker-controlled length overflows a heap buffer (an L10-style bug). Walk each rung and ask: would it have stopped this?

Commit before you compute: what does Placing one real CVE on the spectrum come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Rungs 2–4 each help, none guarantees

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Libraries depend on remembering this site; fuzzing depends on triggering it; mitigations only contain it after it exists.

69. Placing one real CVE on the spectrum

Worked example

A team ships a C image parser. A memcpy with an attacker-controlled length overflows a heap buffer (an L10-style bug). Walk each rung and ask: would it have stopped this?

// the bug:
memcpy(dst, src, attacker_len);   // attacker_len > sizeof(dst)

// rung 1 language:  parser in Rust    -> bounds-checked, no overflow
// rung 2 libraries: bounded copy/check -> stopped IF this site converted
// rung 3 tools:     fuzz + ASan        -> likely FINDS it pre-release
// rung 4 mitigation: ASLR/canary       -> bug exists, exploit made harder

Rung 1 eliminates it outright

Why: In safe Rust the over-long copy can't compile/run as an out-of-bounds write — the class is gone.

Rungs 2–4 each help, none guarantees

Why: Libraries depend on remembering this site; fuzzing depends on triggering it; mitigations only contain it after it exists.

RungWould it stop THIS bug?
memory-safe languageyes — eliminated
safe libraries + defenseonly if this call was converted
fuzzing + sanitizersprobably finds it first
runtime mitigationsno — only harder to exploit

70. Fill in: Would it stop THIS bug? for Placing one real CVE on the spectrum

Comparison

Comparison matrix

From Placing one real CVE on the spectrum: refill the Would it stop THIS bug? column from what you know. The rest of the table is as it appeared.

RungWould it stop THIS bug?
memory-safe languageyes — eliminated
safe libraries + defenseonly if this call was converted
fuzzing + sanitizersprobably finds it first
runtime mitigationsno — only harder to exploit

71. Defense in depth — use the whole ladder

Intuition

Ranking the rungs is not a reason to use only the top one. Real teams stuck with C apply every rung at once — bounded APIs, fuzzing, review, sanitizers, mitigations — because each catches what the others miss.

The ranking tells you only one thing: don't mistake a lower rung for the guarantee of the top rung. Fuzzing hard is great; it still isn't 'we eliminated overflows'.

72. By analogy: Defense in depth — use the whole ladder

Analogy

Discussion prompt

Explain Defense in depth — use the whole ladder by analogy to something with no Computer Security in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

The ranking tells you only one thing: don't mistake a lower rung for the guarantee of the top rung. Fuzzing hard is great; it still isn't 'we eliminated overflows'.

73. Connecting to the small-TCB principle

Intuition

Recall §1.12: keep the trusted computing base (TCB) small. Every line of unsafe C is part of the attack surface — so less unsafe code = smaller TCB = fewer ways in.

A memory-safe language shrinks the TCB by construction: the bug class can't live there. Safer C and tooling only shave the edges of a TCB that stays large.

74. Break it if you can: Connecting to the small-TCB principle

Counterexample

Discussion prompt

Recall §1.12: keep the trusted computing base (TCB) small. Every line of unsafe C is part of the attack surface — so less unsafe code = smaller TCB = fewer ways in.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

A memory-safe language shrinks the TCB by construction: the bug class can't live there. Safer C and tooling only shave the edges of a TCB that stays large.

75. Consolidate

Section

Part 5

76. Without one step: Pattern: choose a memory-safety strategy

Constraint

Discussion prompt

Run Pattern: choose a memory-safety strategy with this step confiscated:

Find what's left: run a fuzzer + ASan/MSan, Valgrind the test suite, gauge coverage, and require human code review.

Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.

Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.

Answer:

  1. Can you pick the language? If yes, choose a memory-safe one (Rust if you also need C-level speed with no GC). This is the only 100% fix — stop here if…
  2. Stuck in C? Replace every unbounded call with its bounded twin: gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and **null-terminate…
  3. Add defensive checks at each access (non-NULL, in-bounds) and compile run-time checks that crash safely on failure.
  4. Find what's left: run a fuzzer + ASan/MSan, Valgrind the test suite, gauge coverage, and require human code review.
  5. Wrap it in process: threat modeling, required security review, pen-testing, a disclosure channel (the SDL).
  6. Remember the ranking: only step 1 eliminates the bug class; steps 2–5 reduce, find, and contain.

77. Pattern: choose a memory-safety strategy

Pattern

  1. Can you pick the language? If yes, choose a memory-safe one (Rust if you also need C-level speed with no GC). This is the only 100% fix — stop here if you can.
  2. Stuck in C? Replace every unbounded call with its bounded twin: gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and null-terminate after strncpy.
  3. Add defensive checks at each access (non-NULL, in-bounds) and compile run-time checks that crash safely on failure.
  4. Find what's left: run a fuzzer + ASan/MSan, Valgrind the test suite, gauge coverage, and require human code review.
  5. Wrap it in process: threat modeling, required security review, pen-testing, a disclosure channel (the SDL).
  6. Remember the ranking: only step 1 eliminates the bug class; steps 2–5 reduce, find, and contain.

78. Where does it stop working: Pattern: choose a memory-safety strategy

Edge cases

Discussion prompt

Pattern: choose a memory-safety strategy works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".

Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.

Answer:

  1. Can you pick the language? If yes, choose a memory-safe one (Rust if you also need C-level speed with no GC). This is the only 100% fix — stop here if…
  2. Stuck in C? Replace every unbounded call with its bounded twin: gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and **null-terminate…
  3. Add defensive checks at each access (non-NULL, in-bounds) and compile run-time checks that crash safely on failure.
  4. Find what's left: run a fuzzer + ASan/MSan, Valgrind the test suite, gauge coverage, and require human code review.
  5. Wrap it in process: threat modeling, required security review, pen-testing, a disclosure channel (the SDL).
  6. Remember the ranking: only step 1 eliminates the bug class; steps 2–5 reduce, find, and contain.

79. Rule out three: Checkpoint — which fix is the 100% guarantee?

Elimination

Eliminate the wrong options

Which approach is the ONLY one that stops 100% of memory-safety vulnerabilities, no matter what the programmer does?

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. Rewrite the service in a memory-safe language such as Rust.
  • B. Replace every gets/strcpy/sprintf with fgets/strncpy/snprintf.
  • C. Add a fuzzer that runs a billion random inputs in CI.
  • D. Switch to Rust only because its garbage collector frees memory safely.

Survives elimination: A

Why: §4.1: a memory-safe language combines compile-time and runtime checks that prevent memory errors regardless of programmer mistakes — the only 100% guarantee. Rust does this via ownership and the borrow checker, with NO garbage collector, so it also keeps C-level performance.

80. Checkpoint — which fix is the 100% guarantee?

Check

A team's C service keeps shipping buffer-overflow CVEs. They list four candidate fixes. Decide which one — and only which one — eliminates the entire bug class. Think it through before clicking.

Check your understanding

Which approach is the ONLY one that stops 100% of memory-safety vulnerabilities, no matter what the programmer does?

  • A. Rewrite the service in a memory-safe language such as Rust. (correct)
  • B. Replace every gets/strcpy/sprintf with fgets/strncpy/snprintf.
  • C. Add a fuzzer that runs a billion random inputs in CI.
  • D. Switch to Rust only because its garbage collector frees memory safely.

Answer: A

Why: §4.1: a memory-safe language combines compile-time and runtime checks that prevent memory errors regardless of programmer mistakes — the only 100% guarantee. Rust does this via ownership and the borrow checker, with NO garbage collector, so it also keeps C-level performance.

Why B tempts people
Safe libraries only protect the call sites you remember to convert — one missed strcpy reopens the hole. It reduces risk; it does not eliminate the bug class.
Why C tempts people
Fuzzing FINDS bugs it happens to trigger but can never prove their absence. A clean fuzzing run raises confidence, not a guarantee.
Why D tempts people
Right language, wrong reason: Rust has NO garbage collector. Its safety comes from ownership and the borrow checker at compile time — which is exactly why it keeps C's deterministic performance.

81. Misconceptions to drop now

Concept

82. Synthesis — this is the fix the whole unit pointed to

Concept

83. Primary sources & where to read more

Concept

84. Connect it up: L11 · Memory-Safe Languages, Safer C, and the Secure Software Process

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Use a Memory-Safe Language · Writing Memory-Safe Code in C · Building Secure Software · The Spectrum of Fixes · Consolidate. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

85. Recap — Lesson 11

Recap

You can explain why a memory-safe language is the only 100% fix, describe Rust's ownership model (safe, no GC), write safer C with bounded libraries, name the tools and process that find remaining bugs, and rank every defense on the spectrum.

IdeaThe one fact
Only 100% fixa memory-safe language (Java/Python/Go/Rust/Swift)
Rustownership + borrow checker — safe with NO garbage collector
Safer Cgets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf
strncpy gotchadoes NOT null-terminate when src fills the buffer
Find what's leftfuzzing, Valgrind, sanitizers, review (find, not prove)
The spectrumlanguage eliminates > libraries reduce > tools find > mitigations contain

Sources

  1. CS 161 Computer Security Textbook §4.1 (Use a memory-safe language), §4.2 (Writing memory-safe code), §4.3 (Building secure software) — Wagner, Weaver, Kao, Shakir, Law & Ngai, UC Berkeley
  2. The Rust Programming Language, Ch. 4 — Understanding Ownership (ownership, borrowing, the borrow checker) ⊕ beyond the book
  3. Sutton, Greene & Amini, 'Fuzzing: Brute Force Vulnerability Discovery' (Addison-Wesley, 2007); orig. Miller, Fredriksen & So, 'An Empirical Study of the Reliability of UNIX Utilities' (CACM, 1990) — Miller et al., Comm. ACM 33(12), 1990
  4. Microsoft Security Development Lifecycle (SDL) — threat modeling, security review, penetration testing, disclosure ⊕ beyond the book

Want this taught 1-on-1? Alexander tutors Computer Security — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108