Skip to content

← Unsafe and Soundness step 2 of 24

Easy Primitives

What unsafe allows, and what it does not change

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

Write the same sum twice.

  • sum_via_raw takes ownership of the vector, gets a raw pointer to its first element with v.as_ptr(), and reads element i through p.add(i).
  • sum_safe borrows a slice and must contain no unsafe code.

Both functions receive the same values, in the same order, and must return the same result. The vector may be empty (the sum is then 0) and may contain any i64, including i64::MIN and i64::MAX. Add with wrapping_add, so a sum that does not fit in i64 wraps around in two's complement instead of panicking in a debug build. For example, [i64::MAX, 1] sums to i64::MIN. The grader prints both results as {"raw": ..., "safe": ...}.

The starter does not compile. It fails with E0133, the error for an unsafe operation used outside an unsafe block or unsafe fn. Read that error before changing anything.

What unsafe allows

Most Rust code is safe Rust, code that does not use unsafe. If safe code compiles, it cannot cause undefined behavior (a program state the language gives no meaning to, such as reading freed memory), provided the unsafe code it calls into, including the standard library's, is correct. A few operations cannot be checked that way. Rust only accepts them inside an unsafe block or unsafe fn, and the programmer becomes responsible for the conditions the compiler cannot verify.

The Rust Book lists five such operations:

  1. dereference a raw pointer (*const T or *mut T);
  2. call an unsafe fn, including most functions declared in an extern block;
  3. read or write a mutable static;
  4. implement an unsafe trait (written unsafe impl);
  5. read a field of a union (assigning to a field is safe).

The Rust Reference, which is the more complete source, also lists:

  1. call a safe function marked #[target_feature] from a function that does not enable the same features;
  2. declare an extern block, which edition 2024 requires to be written unsafe extern "C" { ... };
  3. apply an unsafe attribute such as #[unsafe(no_mangle)] or #[unsafe(export_name = "...")], which edition 2024 also requires to be marked unsafe.

Check the Reference when you need the exact current list. The exercise below uses only the first two.

What unsafe does not change

It does not turn off the borrow checker. This fails inside unsafe exactly as it would outside it:

let mut x = 5;
unsafe {
    let a = &mut x;
    let b = &mut x;   // error[E0499]: cannot borrow `x` as mutable more than once at a time
    *a += 1;
    *b += 1;
}

a is still used after b is created, so two mutable references to x are live at once. The compiler also warns that this unsafe block is unnecessary, because nothing inside it is an unsafe operation.

It does not turn off type checking. let x: u32 = "hello"; is still error E0308 (mismatched types) inside unsafe.

It does not turn off lifetime checking. Returning a reference to a local variable, as in fn f() -> &'static i32 { let x = 1; &x }, is still error E0515 with the body wrapped in unsafe.

It does not make incorrect code correct. unsafe does not grant permission to break Rust's rules. It moves the job of upholding them from the compiler to you. If the conditions are not met, for example a raw pointer points outside its allocation, the behavior is undefined: the program may appear to work, crash, or be optimized in ways that make its output meaningless.

Check your understanding A function fails to compile with E0499. Someone wraps the Show answer

offending lines in unsafe { }. What happens? Nothing changes. The program still fails with E0499.

unsafe only permits the specific operations listed above. The borrow checker, type checker and lifetime checks apply inside unsafe blocks with the same rules as everywhere else.

Borrow errors such as E0499, E0502, E0505, E0515 and E0597 are usually fixed by restructuring the code: shortening a borrow, splitting a slice with split_at_mut, using a type such as RefCell that checks borrows at run time, or changing a function signature.

Why the exercise has two functions

sum_safe is one short line with .iter() and .fold(). It needs no unsafe, because a slice iterator stays within the slice for you.

sum_via_raw computes the same value, but every read is an operation whose safety you have to argue for yourself. The comparison shows that this task never needed unsafe. Code that uses unsafe should have a reason that safe Rust cannot meet, such as calling a C function or managing raw memory inside a data structure, and each unsafe block adds conditions someone has to check.

The lints in this problem

The grade is: the code compiles, the tests pass, and cargo clippy -- -D warnings reports nothing. -D warnings turns every warning into an error.

The starter enables clippy::undocumented_unsafe_blocks with #![warn(...)]. Every unsafe { } block needs a comment starting with // SAFETY: above it that explains why the operation is valid. The comment must come directly before the statement containing the block; a blank line in between is accepted, but another line of code is not. Item 17.4 covers how to write these comments well.

The compiler's own unused_unsafe lint warns about an unsafe block that contains no unsafe operation. A block that wraps the whole loop does contain one, so that lint stays quiet. Wrap only the expression that needs unsafe anyway: the smaller the block, the less code the SAFETY comment has to justify.

Check your understanding In unsafe { *p.add(i) }, which operations need unsafe? Show answer

Both p.add(i) and the * dereference.

* on a raw pointer is operation 1: dereferencing a raw pointer.

p.add(i) is operation 2: <*const T>::add is declared unsafe fn. Its documented requirement is that the resulting pointer stays within the same allocation (or one element past its end). Computing an address outside that range is undefined behavior even if the pointer is never dereferenced. wrapping_add is the safe method for offsets that may leave the allocation, but dereferencing its result still requires unsafe and a valid pointer. Item 17.7 covers pointer arithmetic in detail.

So unsafe { *p.add(i) } contains two unsafe operations. The Clippy restriction lint multiple_unsafe_ops_per_block would require two separate blocks, each with its own justification. It is not enabled here; item 17.4 enables it.