Rust the ownership model & beyond
move · O overview · F fit
01 / 00

A mental model, not a syntax tour

Learning Rust.
Ownership is the whole idea.

Rust gives you C-level control and memory safety, with no garbage collector — by making the compiler prove your program is safe before it runs. The price is one big idea you have to internalize: ownership. Get that, and the borrow checker stops fighting you and starts catching your bugs. We build it from first principles, then climb to traits, errors, and fearless concurrency.

ownership & movesborrowingthe borrow checker lifetimestraitsResult / ?fearless concurrency

Five acts · ~21 stops · drive the borrow checker, scrub a lifetime, watch a move invalidate a value. Real examples cameo from a production Rust codebase (NetWatch).

What you get, and what it costs

Memory safety without a garbage collector

Most languages pick one: manual memory (C/C++ — fast, footguns) or a garbage collector (Go/Java — safe, runtime cost). Rust refuses the trade by moving the bookkeeping to compile time.

  • No GC, no pauses. Memory is freed at a deterministic, known point — when its owner goes out of scope.
  • No use-after-free, no data races. The compiler rejects them — they're not runtime bugs you hunt, they're build errors.
  • "Fearless" is literal. The confidence comes from the compiler, not from your discipline or your test coverage.

The cost is real: you learn to think in ownership, and the compiler says "no" a lot at first. This deck is about making those "no"s legible.

C/C++ — control, but UAF & races at runtime
vs
Go/Java — safe, but a GC at runtime
Rust:
control + safety, proven at COMPILE time

The borrow checker is a theorem prover for "no aliasing bug." Pass it, and a whole class of CVEs is simply impossible in safe Rust.

Three rules, and everything follows

Every value has exactly one owner

The entire system rests on three rules the compiler enforces. Memorize these; the rest of Rust is consequences of them.

  • 1. Each value has one owner — a single variable binding responsible for it.
  • 2. One owner at a time — assign or pass it, and ownership moves; the old binding is no longer valid.
  • 3. Owner drops → value freed — when the owner leaves scope, the value's Drop runs and its memory is released. Deterministically.

"Borrowing" (next act) is how you let other code use a value without taking ownership — the escape valve that makes rule 2 livable.

ownership & scope

Stack values that are cheap to duplicate (i32, bool, char…) are Copy: assigning them copies. Everything heap-backed (String, Vec…) moves.

Three ways a value travels

The difference between move, copy, and clone is the difference between fast and safe

Move default for owned

Assigning or passing a heap value transfers ownership — no data is copied, just the pointer. Cheap (a few bytes), but the source binding is invalidated so two owners can never free the same memory.

Copy cheap stack types

Types that are trivially duplicable (i32, bool, small Copy structs) are copied on assignment. Both bindings stay valid because each has its own independent value.

Clone explicit deep copy

.clone() makes an independent deep copy on purpose — you opt into the cost. The source stays valid because you now have two separate owners of two separate allocations.

Borrow use without owning

Often you don't need to move or copy — you just want to read or mutate in place. That's a reference (& / &mut) — the whole of Act II.

The art of idiomatic Rust is largely "borrow by default, move when you must, clone when you mean it." Reflexive cloning to dodge the borrow checker is the classic beginner smell.

Watch what happens to the source binding

After a move, the original is gone — and the compiler knows

The single most common first-week error is use of moved value. Pick what t does to s and see whether the final println! compiles.

explore

let s = String::from("netwatch");

does it compile?

A move is just a pointer handoff plus a compile-time note that the source is dead. No runtime cost, no runtime check — the safety is entirely in the type system.

Use a value without taking it

Shared & XOR exclusive &mut

Moving everything everywhere would be miserable. References let you lend access. The one rule that makes them safe — and that the borrow checker enforces:

  • Any number of shared & references (read-only), OR
  • exactly one exclusive &mut reference (read-write) —
  • never both at once. Aliasing XOR mutation. That single invariant is what prevents data races and iterator invalidation.

A reference borrows; it never owns. The borrow must end before the owner is moved or dropped — which is what lifetimes (slide 8) track.

shared vs exclusive

The compiler, simulating your references

Step the checker — then break the rule on purpose

The borrow checker walks your code tracking which references are live at each point. Pick a scenario and step it: watch the live borrows, and watch a real error fire the moment aliasing meets mutation.

drive it

borrow check: let mut v = vec![1, 2, 3];

These errors feel like obstacles for a week, then become a sixth sense. The checker is catching the exact bugs that are heisenbugs in C — aliased mutation — at compile time.

A reference may not outlive what it points to

Lifetimes are the spans the checker compares

A lifetime is just the region of code where a reference is valid. The rule is simple: a borrow must not outlive its owner. Scrub the timeline — in the dangling case, watch the owner drop while the reference is still around.

drive it

does the borrow outlive the owner?

Most lifetimes are elided — the compiler infers them. You only write 'a annotations when a function returns a reference and the compiler can't tell which input it borrows from.

A borrowed view into someone else's buffer

Slices are references — so dangling is impossible

A slice (&[T], &str) is a borrowed window — a pointer and a length — into a Vec or String it doesn't own. The borrow checker ties the window's lifetime to the buffer's.

  • Hold a slice into a Vec, and you cannot mutate that Vec (e.g. push, which might reallocate) while the slice lives.
  • That's the same aliasing-XOR-mutation rule preventing the classic C bug: a pointer left dangling by a realloc.

The bug doesn't need a sanitizer to find at runtime — it doesn't compile.

the iterator-invalidation bug, prevented

Make illegal states unrepresentable

Enums carry data; match must handle every case

Rust enums are sum types: each variant can hold its own data. Combined with exhaustive match, you encode "exactly one of these, with its payload" — and the compiler won't let you forget a case.

  • A variant can be empty, or carry a tuple/struct of data — model the real shape of your domain.
  • match is exhaustive: miss a variant and it's a compile error, not a bug that ships.
  • Matching binds the payload, so you handle the data and the case together.

This is how Option and Result are built — they're just enums in the standard library.

netwatch — src/ebpf/mod.rs

No exceptions — failure is in the type

Result<T, E> and the ? operator

There are no exceptions to forget to catch. A function that can fail says so in its return type: Result<T, E> — either Ok(value) or Err(why).

  • The signature is honest: you can't ignore a possible failure, because you have to unwrap the Result to get the value.
  • ? is the ergonomic glue: "if this is Err, return it from the whole function; otherwise give me the Ok value."
  • Errors propagate up the call stack as values, explicitly — no invisible control flow.

panic! exists for truly unrecoverable bugs — but recoverable failure is always a Result.

netwatch — the kprobe, simplified

Shared behavior, without inheritance

Traits define what a type can do

A trait is a set of methods a type can implement — Rust's answer to interfaces and the basis of generics. NetWatch's DPI layer is one trait, implemented 15 times.

  • Define once, implement per type. Each protocol classifier impls the same Classifier trait.
  • Static dispatch (<C: Classifier>) — the compiler generates a specialized copy per type. Zero runtime cost.
  • Dynamic dispatch (&dyn Classifier) — one vtable, chosen at runtime. For heterogeneous collections.

Bounds like T: Send + Clone say "any type that can do these" — generic code that's still fully type-checked.

netwatch — src/dpi/mod.rs

The billion-dollar mistake, designed out

Absence is a type: Option<T>

Rust has no null. A value that might be missing has type Option<T> — either Some(v) or None — and the compiler forces you to handle the None case before you can touch the value.

  • No more accidental null dereferences — the absent case is visible in the type and checked at compile time.
  • Rich combinators instead of null checks: map, unwrap_or, and_then, ok_or.
  • ? works on Option too — propagate "nothing here" cleanly.
handling absence, explicitly

The functional core — lazy and zero-cost

Chains that compile down to a tight loop

Iterator adapters (filter, map, …) are lazy: they build a recipe and do nothing until a consumer like collect pulls. Each element is dragged through the whole chain, one at a time. Step it.

drive it

(1..=6).filter(|n| n%2==0).map(|n| n*n).collect()

source
n = –
1..=6, pulled one at a time
filter
even?
keep n % 2 == 0
map
n * n
square the survivors
collect
Vec
drives the whole chain
result: []

Zero-cost abstraction: this chain optimizes to the same machine code as a hand-written for loop. Closures (|n| n*n) capture their environment and cost nothing extra.

When single-owner-on-the-stack isn't enough

Heap, sharing, and interior mutability — opt in by type

Box<T> heap, one owner

Put a value on the heap with a single owner. For recursive types, large values, or storing a dyn Trait.

Rc<T> shared, 1 thread

Reference-counted shared ownership within one thread. The value drops when the last Rc does. Not thread-safe (cheap, non-atomic count).

Arc<T> shared, threads

Atomic Rc — shared ownership across threads. The "A" is the only difference, and it's what makes it Send.

RefCell<T> runtime borrow check

Interior mutability: mutate through a shared reference, with the borrow rules checked at runtime (panics on violation) instead of compile time.

Arc<Mutex<T>> ← netwatch

The standard "shared mutable state across threads" combo. NetWatch's collectors live behind exactly this — the next slide.

The pattern

Reach for these only when ownership truly needs to be shared or deferred. Most code is plain owned values and borrows — these are the deliberate exceptions.

Data races are a compile error

The ownership rules extend straight to threads

Aliasing-XOR-mutation isn't just for one thread — it's exactly the rule that prevents data races. The compiler tracks two auto-traits, Send (safe to move to another thread) and Sync (safe to share), and refuses unsafe sharing. Toggle the two approaches:

explore

two threads, one piece of data

does it compile?

NetWatch's collectors share state as Arc<Mutex<T>> and stream events over an mpsc channel — the compiler guarantees no collector thread can race the UI thread. That guarantee is free at runtime.

Cleanup is tied to ownership

When the owner goes, the cleanup runs — deterministically

There's no finally, no defer, no garbage-collector finalizer-someday. A value's Drop runs the instant its owner leaves scope — including on early return or panic.

  • RAII everywhere. A file closes, a lock unlocks, a socket detaches — because the guard owning it was dropped.
  • You rarely write Drop by hand; you compose types that already clean up after themselves.
  • Deterministic, not eventual. You know exactly when resources are released — the closing brace.

This is the same mechanism behind MutexGuard unlocking and NetWatch's eBPF handle detaching the kprobe.

netwatch — src/ebpf/source.rs

The reason day-to-day Rust is pleasant

One tool, batteries included

cargo is build system, package manager, test runner, and doc generator in one. The cohesion is a feature: every project works the same way, and the ecosystem composes.

  • crates.io — one registry; cargo add and a version is wired in, dependencies resolved.
  • clippy — 600+ lints that catch real bugs and teach idioms as you go.
  • rustfmt — one canonical style; formatting debates simply don't happen.
  • rust-analyzer — a world-class language server: types, completions, and the borrow checker's reasoning live in your editor.

Tests and docs are first-class: cargo test runs unit, integration, and even the examples inside your doc comments.

a day with cargo

The escape hatch — and what it does NOT turn off

unsafe is a promise, not a free-for-all

Sometimes you must do what the compiler can't verify — talk to hardware, FFI, or read raw kernel memory. unsafe unlocks exactly five extra powers, and nothing else.

  • It lets you dereference raw pointers, call unsafe fns, and a few more — that's the whole list.
  • It does not disable the borrow checker, type checking, or ownership for everything else.
  • It's a promise: "I've upheld the invariants the compiler can't see." Your job is to wrap it in a sound safe API.

Good Rust isolates unsafe to tiny, audited blocks behind safe interfaces — so the unsafety is contained, not spread.

netwatch — the eBPF kprobe

The right tool, and how to keep learning

Where Rust earns its learning curve

Rust shines where correctness and control both matter — and the ownership tax pays for itself.

  • Systems & infra — networking, databases, OS components, embedded. Predictable performance, no GC pauses.
  • Anything concurrent — the compiler eliminates data races, so aggressive parallelism is safe.
  • Long-lived, high-stakes code — the type system encodes invariants that survive refactors and new maintainers.
  • CLI / TUI tools — fast startup, single static binary, great libraries (exactly NetWatch's niche).

Reach for a GC'd language when iteration speed beats control and a few ms of pause don't matter. Rust is a deliberate trade, not a default.

Keep learning

The Book (doc.rust-lang.org/book) — the canonical intro. Rustlings — fix-the-code exercises. Rust by Example — runnable snippets. std docs — superb and searchable.

The way in is to write it

Port a small tool you already understand. Let the compiler teach you ownership through its errors — they're unusually good. Lean on clippy and rust-analyzer as live tutors.

The whole map, once more

It all comes back to ownership

The bargain Act I

Safety without a GC, proven at compile time. One value, one owner; move, copy, or clone — borrow by default.

Ownership Act II

Shared & XOR exclusive &mut; the borrow checker enforces it; lifetimes ensure a borrow never outlives its owner.

Types that work Act III

Enums + exhaustive match; errors as Result with ?; traits for shared behavior; Option instead of null.

Real work Act IV

Lazy zero-cost iterators; smart pointers for sharing; data races as compile errors; deterministic Drop.

Ecosystem Act V

cargo + clippy + rust-analyzer; and unsafe as a small, audited promise — not a disabling of the rules.

The throughline

Every feature is a consequence of ownership. Once it clicks, the borrow checker is a collaborator, not an obstacle — it's catching your bugs at compile time.

Press O for the full map · to revisit any simulation. The fight with the borrow checker is the learning — and it ends.

Jump to a stop — click any, or press O to close