Skip to content

← Unsafe and Soundness step 2 of 24

Easy Primitives

The five superpowers — and the four myths

pub fn sum_via_raw(v: Vec<i64>) -> i64
pub fn sum_safe(v: &[i64]) -> i64

Write the same function twice. sum_via_raw must sum the vector by taking a raw pointer to its buffer and walking it with .add(i). sum_safe must sum it with no unsafe at all. Both are graded, on the same inputs, and they must agree. Use wrapping addition in both so the 10 000-element case cannot overflow into a difference.

The starter does not compile. The error is E0133, and it is the first thing you need to see.

What unsafe actually unlocks

Rust is two languages sharing one syntax. In Safe Rust the compiler proves memory safety for you. In Unsafe Rust you prove it, and the unsafe keyword is where you sign your name.

The Book gives five superpowers, and they are worth memorising because the list is short and closed — there is nothing else unsafe lets you do:

  1. dereference a raw pointer (*const T / *mut T);
  2. call an unsafe fn;
  3. access or modify a mutable static;
  4. implement an unsafe trait;
  5. access the fields of a union.

The Rust Reference is the authority, and it lists three more that the Book omits:

  1. call a safe #[target_feature] function from a function that does not have those features enabled;
  2. declare an extern block (this became an unsafe operation in edition 2024 — unsafe extern "C" { ... });
  3. apply an unsafe attribute to an item (#[unsafe(no_mangle)], #[unsafe(export_name = "...")]).

Learn the Reference’s eight; keep the Book’s five as the mnemonic. Everything in this track is an instance of one of them.

The four myths

This is the most mis-stated fact about Rust, and getting it wrong is how people write unsafe code that “works” for years and then miscompiles.

unsafe does not turn off the borrow checker. It runs identically inside an unsafe block. Try it:

let mut x = 5;
unsafe {
    let a = &mut x;
    let b = &mut x;   // E0499 — cannot borrow `x` as mutable more than once
    *a += 1;
    *b += 1;
}

That is a hard error, inside unsafe, exactly as it is outside. If your real problem is a borrow error, unsafe is not the tool.

unsafe does not turn off type checking. let x: u32 = "hello"; is still E0308.

unsafe does not turn off lifetime checking. Returning a reference to a local is still E0106/E0515.

unsafe does not make anything you write legal. It does not grant permission; it transfers the obligation. The compiler stops asking you to prove memory safety and starts assuming you have. If you have not, the program has no defined meaning at all.

::: question A learner has a function that will not compile because of E0499. They wrap the offending lines in unsafe { }. What happens? Nothing changes. The program still fails to compile with E0499.

unsafe widens the set of operations you are allowed to name — the eight above. It does not weaken any static analysis. The borrow checker, the type checker and the lifetime checker all run over unsafe blocks with exactly the rules they use everywhere else.

This is the single most useful thing to know about unsafe, because it tells you when not to reach for it. E0499, E0502, E0505, E0515, E0597 — none of those are jobs for unsafe. They are jobs for restructuring, for split_at_mut, for RefCell, or for changing the signature. :::

Why this exercise ships two functions

sum_safe is the point of the problem. You will write it in one line, with a fold or .sum(), and it will compile to the same machine code as the raw version — probably better, because the iterator carries the length in its type and LLVM knows it cannot alias.

So the exercise you thought was about pointers is really about calibration: unsafe was never needed here, and reaching for it would have bought you a proof obligation in exchange for nothing.

Keep that ratio in mind for the rest of the track. Real unsafe code exists to do things safe Rust genuinely cannot express — build a Vec out of raw memory, talk to C, implement a lock — and every one of those has a cost you must be getting something for.

The gate teaches minimal scope

Two lints are on for this problem, and they are not decoration.

clippy::undocumented_unsafe_blocks is enabled by the template. Every unsafe { } you write needs a // SAFETY: comment on the immediately preceding line — no blank line in between, or the lint does not see it. Item 17.4 is about why that discipline is the difference between auditable and unauditable code; here it is just muscle memory.

rustc’s unused_unsafe is warn-by-default, and because the grade is clippy -D warnings, a warning is a failure. An unsafe block containing no unsafe operation is a lie about your code, and the compiler says so. Wrap the smallest expression that actually needs it.

::: question p.add(i) and *p are both inside your unsafe block. Which one is the superpower? Both, but for different reasons — and this matters later.

*p is superpower #1: dereferencing a raw pointer.

p.add(i) is superpower #2: <*const T>::add is declared unsafe fn, because computing an out-of-bounds address is already undefined behaviour even if you never dereference it. That rule surprises everyone and item 17.7 is about it.

So unsafe { *p.add(i) } contains two unsafe operations, not one. Clippy has a restriction lint, multiple_unsafe_ops_per_block, that would insist you split them into two blocks with two separate justifications. It is off here — item 17.4 turns it on, once, deliberately, because it is the problem that is about it. :::

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…