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
Title
CS 161 · Lesson 11 of 45
memory-safe languages · Rust ownership · safer C libraries · fuzzing · Valgrind · the secure-development process
Objectives
gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf.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.
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.
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.
Section
Part 1 · §4.1
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.
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 persists | Real? |
|---|---|
| Legacy code already written in C | yes — install base never shrinks |
| Perceived performance need | mostly perception |
| Memory safety requires giving up speed | false — see Rust |
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 persists | Real? |
|---|---|
| Legacy code already written in C | yes — install base never shrinks |
| Perceived performance need | mostly perception |
| Memory safety requires giving up speed | false — see Rust |
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.
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.
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.
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.
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 class | C | Safe Rust |
|---|---|---|
| use-after-free | silent UB at runtime | compile error (lifetime) |
| buffer overflow | silent corruption | bounds-checked / rejected |
| mutable aliasing | data race / corruption | compile error (borrow rules) |
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 class | C | Safe Rust |
|---|---|---|
| use-after-free | silent UB at runtime | compile error (lifetime) |
| buffer overflow | silent corruption | bounds-checked / rejected |
| mutable aliasing | data race / corruption | compile error (borrow rules) |
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.
| Language | Main mechanism | Runtime GC? |
|---|---|---|
| Java / Python | runtime checks + GC | yes |
| Go | runtime checks + GC | yes |
| Rust | compile-time ownership | no |
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.
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.
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 knownC 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.
| World | Out-of-bounds index | Outcome |
|---|---|---|
| C | buf[10] | silent corruption — exploitable |
| Python | lst[10] | IndexError — safe failure |
| Rust | v[10] | panic / compile error — safe failure |
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.
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.
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.
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.
Section
Part 2 · §4.2
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.
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.
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.
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.
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.
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.
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 API | Safe replacement | What 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 |
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 API | Safe replacement | What 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 |
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.
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.
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.
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.
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.
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.
| Call | Max bytes written | Reaches rip? |
|---|---|---|
| gets(buf) | unbounded | yes — hijack |
| fgets(buf, sizeof(buf), stdin) | 7 + '\0' = 8 | no — capped at buf |
| fgets(buf, 8, stdin) // literal | 8 | no, but fragile if buf grows |
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.
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.
Section
Part 3 · §4.3
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.
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.
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.
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.
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).
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
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.
Ranking
Put in order
Put the moves of What a fuzzer is doing, conceptually into the order they have to happen.
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.
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 investigateGenerate 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 outcome | What it proves |
|---|---|
| crash found | a bug exists (a real one) |
| no crash yet | nothing — just not found yet |
| ran 10^9 inputs clean | high confidence, NOT a proof |
Cost model
Annotate
In What a fuzzer is doing, conceptually, before reading the notes: mark where the time actually goes. Which line dominates?
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.
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 style | Strategy | Effectiveness |
|---|---|---|
| dumb / black-box | pure random mutation | finds shallow bugs |
| coverage-guided | keep inputs hitting new paths | reaches deep bugs |
| coverage-guided + ASan | + detect silent corruption | best for memory bugs |
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 style | Strategy | Effectiveness |
|---|---|---|
| dumb / black-box | pure random mutation | finds shallow bugs |
| coverage-guided | keep inputs hitting new paths | reaches deep bugs |
| coverage-guided + ASan | + detect silent corruption | best for memory bugs |
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
strncpy(dst, src, sizeof(dst)) and then treating dst as a C string.Section
Part 4 · Synthesis
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.
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.
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.
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.
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 runtimeOnly 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.
| Rung | Verb | 100%? |
|---|---|---|
| memory-safe language | eliminates | yes |
| safe libraries + defensive code | reduces | no |
| tools / fuzzing / review | finds | no |
| runtime mitigations (next unit) | contains | no |
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.
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.
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 harderRung 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.
| Rung | Would it stop THIS bug? |
|---|---|
| memory-safe language | yes — eliminated |
| safe libraries + defense | only if this call was converted |
| fuzzing + sanitizers | probably finds it first |
| runtime mitigations | no — only harder to exploit |
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.
| Rung | Would it stop THIS bug? |
|---|---|
| memory-safe language | yes — eliminated |
| safe libraries + defense | only if this call was converted |
| fuzzing + sanitizers | probably finds it first |
| runtime mitigations | no — only harder to exploit |
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'.
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'.
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.
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.
Section
Part 5
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:
gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and **null-terminate…Pattern
gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and null-terminate after strncpy.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:
gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf — and **null-terminate…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.
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.
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?
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.
Concept
strlcpy.Concept
Concept
strcpy-based C program with -fsanitize=address, fuzz it, then rewrite the same logic in Rust and watch the bug become a compile error.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.
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.
| Idea | The one fact |
|---|---|
| Only 100% fix | a memory-safe language (Java/Python/Go/Rust/Swift) |
| Rust | ownership + borrow checker — safe with NO garbage collector |
| Safer C | gets→fgets, strcpy→strncpy/strlcpy, sprintf→snprintf |
| strncpy gotcha | does NOT null-terminate when src fills the buffer |
| Find what's left | fuzzing, Valgrind, sanitizers, review (find, not prove) |
| The spectrum | language eliminates > libraries reduce > tools find > mitigations contain |
Want this taught 1-on-1? Alexander tutors Computer Security — $55/session, free consultation.