Introduction to Rust

Systems Programming and Memory Safety

Systems programming is the discipline of building software that interacts directly with hardware, operating system kernels, and raw memory—including operating systems, device drivers, file systems, database engines, browser engines, embedded firmware, and network stacks. Memory safety is the guarantee that a program can never access invalid, uninitialized, or freed memory locations. Rust unites both by enforcing strict memory and concurrency safety at compile time without a garbage collector.

Characteristics of Systems Programming

Unlike application-level scripting or managed enterprise software, systems software operates under tight resource and timing constraints:

Requirement Why Systems Software Needs It
Direct memory layout control Hardware registers, network packet headers, and C ABI structs require exact byte alignments and contiguous layouts without hidden object headers
Predictable, deterministic latency Real-time audio, kernel interrupt handlers, and high-frequency storage engines cannot pause for stop-the-world garbage collection cycles
Minimal or zero runtime footprint Microcontrollers, bootloaders, and OS kernels (#![no_std]) run on bare metal where no operating system or language virtual machine exists
Fine-grained concurrency Multi-core systems require shared-memory threads, lock-free atomics, and custom synchronization primitives without a global interpreter lock (GIL)

Historically, meeting these requirements meant writing in C or C++, which grant unrestricted pointer access at the cost of exposing programs to memory-corruption vulnerabilities.

Categories of Memory Safety Bugs

Across large C and C++ codebases (such as Chromium, Android, Windows, and Linux), security researchers and project maintainers have consistently found that roughly 70% of high-severity security vulnerabilities stem from memory unsafety. These defects fall into three classes:

                        Memory & Concurrency Bugs
                                    |
        +---------------------------+---------------------------+
        |                           |                           |
        v                           v                           v
Spatial Memory Unsafety    Temporal Memory Unsafety     Data Races (Concurrency)
- Buffer overflow / write  - Use-after-free             - Unsynchronized concurrent
- Buffer over-read         - Double free                  read + write to same
- Out-of-bounds indexing   - Dangling stack pointer       memory address
- Wild / null pointers     - Iterator invalidation

Spatial Memory Safety

Spatial safety requires that every memory access stays strictly within the bounds of the allocated object it targets:

Spatial Violation What Happens in C / C++ How Safe Rust Prevents It
Buffer overflow (out-of-bounds write) Overwrites adjacent stack frames, return addresses, or heap metadata (remote code execution risk) Bounds-checked slice/array indexing (slice[i] panics cleanly on out-of-bounds; .get(i) returns Option<&T>)
Buffer over-read Leaks adjacent secrets from memory (such as the OpenSSL Heartbleed bug) Slices (&[T]) store both a pointer and an explicit len checked on every read
Null pointer dereference Segmentation fault or undefined behavior when compiler assumes pointer is non-null References (&T, &mut T) can never be null; optional values use Option<T> verified by pattern matching
Uninitialized memory read Reads leftover bytes from previous stack frames or heap allocations Compiler control-flow analysis rejects reading any variable before it is definitely initialized (E0381)

Temporal Memory Safety

Temporal safety requires that a memory location is only accessed while the allocation remains alive and valid:

Temporal Violation What Happens in C / C++ How Safe Rust Prevents It
Use-after-free Dereferencing a pointer after free() or delete reads/corrupts reused heap memory Borrow checker ensures no reference (&T) can outlive the owner of the value (E0597)
Double free Calling free() twice on the same address corrupts the allocator's free list Each value has a single owner; moving ownership invalidates the previous variable (E0382), so Drop runs exactly once
Dangling pointer to local stack variable Returning &local from a function points to a destroyed stack frame Compiler forbids returning references to local variables (E0515)
Iterator / reference invalidation Appending to a std::vector reallocates its heap buffer while old element references still point to the freed buffer Holding an immutable element reference (&v[0]) forbids calling v.push(...) (&mut v) simultaneously (E0502)

How Rust Enforces Memory Safety Without a Garbage Collector

Garbage-collected languages (Java, Go, C#, Python) achieve temporal memory safety at runtime by scanning memory periodically to discover and free unreachable heap objects. Rust achieves both spatial and temporal safety at compile time through three interlocking compile-time rules:

  1. Single Ownership and Deterministic Destruction: Every value is owned by one variable binding at a time. When that owner goes out of scope (and has not transferred ownership elsewhere), the compiler automatically inserts a cleanup call (Drop::drop) at that exact point in the machine code.
  2. Aliasing XOR Mutability (The Borrow Checker): At any given point in execution, a value may have either any number of immutable shared references (&T) or at most one mutable exclusive reference (&mut T), never both simultaneously.
  3. Lifetimes: The compiler tracks the scope for which every reference is valid and rejects any program where a reference could outlive the data it points to.
Compile-Time Verification Pipeline in rustc:

  Rust Source Code (.rs)
          |
          v
  +----------------------------------------------------+
  | Type Checker + Borrow Checker (MIR Analysis)       |
  | - Tracks ownership moves & definite initialization |
  | - Verifies &T / &mut T exclusivity rules           |
  | - Proves reference lifetimes never outlive owners  |
  +----------------------------------------------------+
          |
          |  (All ownership & lifetime annotations erased after check!)
          v
  +----------------------------------------------------+
  | Native Machine Binary                              |
  | - Direct stack and heap instructions               |
  | - Exact deallocation points inserted statically    |
  | - Zero garbage collector threads or runtime pauses |
  +----------------------------------------------------+

Preventing Iterator Invalidation at Compile Time

Consider holding a reference to the first element of a growable vector while appending a new element. Because appending may allocate a larger heap buffer and free the old one, Rust rejects the code at compile time:

// ❌ does not compile
fn main() {
    let mut numbers = vec![10, 20, 30];
    let first: &i32 = &numbers[0]; // Immutable borrow of `numbers` starts here

    numbers.push(40); // Error: requires mutable borrow while `first` is still in use!

    println!("First element: {first}");
}
error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
3 |     let first: &i32 = &numbers[0];
  |                        ------- immutable borrow occurs here
4 |
5 |     numbers.push(40);
  |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 |
7 |     println!("First element: {first}");
  |                              ------- immutable borrow later used here

error: aborting due to 1 previous error

If first is read and finished before numbers.push(40) is called, the compiler's non-lexical lifetime analyzer sees that the immutable borrow has ended and allows the mutation:

fn main() {
    let mut numbers = vec![10, 20, 30];
    let first: &i32 = &numbers[0];
    println!("First element before push: {first}"); // Last use of `first`

    numbers.push(40); // Allowed: `first` is no longer live
    println!("Updated vector: {numbers:?}");
}
First element before push: 10
Updated vector: [10, 20, 30, 40]

Role of unsafe in Systems Programming

Low-level systems work sometimes requires operations whose safety depends on hardware specifications or external C contracts that no compiler can verify statically—such as writing to a memory-mapped I/O register at physical address 0x4000_0000, implementing an OS page table, or calling a C system call.

Rust provides the unsafe keyword as an explicit boundary for these operations:

Boundary Guarantees and Responsibilities
Safe Rust (default) The compiler guarantees that no undefined behavior or memory corruption can occur (assuming underlying unsafe building blocks are sound)
unsafe { ... } block Allows dereferencing raw pointers (*const T, *mut T), calling unsafe fn, accessing mutable statics or union fields; the author of the unsafe block takes responsibility for upholding memory invariants

By confining raw pointer manipulation to small, auditable unsafe blocks wrapped inside safe APIs, even if a memory bug occurs in a low-level driver or data structure, investigation is narrowed purely to the unsafe blocks rather than the entire codebase.