---
title: '`unsafe` Rust'
source: 'https://academia.sh/en/courses/rust-advanced/unsafe-rust'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows by running code within the same block that an unsafe block does not close off the compiler's checking wholesale, only for a few specific operations"
    - 'Proves by comparison that a raw pointer read, an unchecked access, and a contracted function call all produce the same result as their safe counterpart'
    - 'Explains, by distinguishing the two, that a delegated guarantee stays invisible in the result when used correctly, and whose responsibility a misuse falls to'
---

# `unsafe` Rust

That an unsafe block does not close off the compiler's entire checking, only the checking that covers a few specific operations, is shown by running code: within the same block, a raw pointer read stays unchecked while a slice query is still checked.

The previous topic showed that no matter how different the body a macro produces looks,
it still passes through type checking like ordinary source: the compiler checked a
generated function exactly as strictly as a hand-written one. This topic asks the same
question from the opposite direction: is there a form where the compiler does not perform
this checking at all, where it stops checking and leaves the matter to the writer? The
answer is the `unsafe` keyword — but this lesson's finding is that the keyword does not
open as wide a door as it appears to.

The Borrow Checker lesson, from the Ownership and the Memory Model course, established how
the compiler tests ownership and borrowing rules, which forms it rejects. This lesson does
not show that checker closing — it shows the opposite: that it does not close, only that
the obstacle in front of a few operations is lifted. The Standard Library and Streams
course asked who gives the guarantee for a behavior: the interface, the implementation, or
the caller — the parties to a contract. What this lesson asks is different: where the
compiler's own checking stops, without looking at either party to that contract.

This gap exists for a reason: the rules the borrow checker tests do not cover every
correct program that could be built. A structure such as a circular buffer, a node pool,
or a driver that accesses a hardware register directly can carry an ownership pattern more
complex than the borrow checker can follow — the checker may reject that pattern even when
it is correct, because its testing rule comes not from the pattern itself but from the
class of patterns the checker can follow. `unsafe` is the escape valve that closes this
gap: the writer can say "this is correct" for a reason the checker cannot test. The
standard library's own growable array and slice types are built the same way — while they
present a safe interface to code that looks from the outside, their own bodies use the
same raw pointer operations. This lesson does not look inside those bodies; it only
measures how the same permission works in an ordinary program.

## The `unsafe` Block's Limited Permission

Constructing a raw pointer does not require `unsafe`. The form `&value as *const i32` is
an ordinary expression, it compiles, and it skips no checking — what is constructed is
only an address, not yet read. Borrow rules are not in play on this line either, because
raw pointers are not among the types the borrow checker tracks; no bookkeeping such as
ownership, lifetime, or uniqueness is kept for them.

The restriction begins when there is an attempt to **read** the pointer. The compiler
cannot know that a genuinely valid value sits where a raw pointer points — the pointer
might be null, the memory it belongs to might have been returned, or it might just carry
a made-up address. Because the compiler cannot distinguish among these three
possibilities, it requires the read to sit inside an `unsafe` block: the code compiles,
but the declaration "there is a contract here and I am meeting it" belongs to the writer.
The same restriction applies to calling a function whose body is marked `unsafe` — the
function's name says the caller must satisfy a precondition, but the compiler does not
test that condition.

This permission list is short and fixed by the language itself: the compiler reads which
operations require `unsafe` from the source, not from the caller's intent. The run below
places three operations that do not require `unsafe` side by side: constructing a raw
pointer, shifting it in a way defined against overflow, and comparing two pointers with
each other. Because none of the three reads the value, they break nothing even if they
operate on an invalid address — what would break something is attempting to access the
data at that address.

```rust
// allowed.rs — which pointer operations do not require unsafe
fn main() {
    let v = vec![10, 20, 30, 40];
    let pointer = v.as_ptr();                       // construct
    let shifted = pointer.wrapping_add(1);           // shift, defined against overflow
    let equals_self = pointer == pointer;
    let equals_shifted = pointer == shifted;

    println!("equals self: {}", equals_self);
    println!("equals shifted: {}", equals_shifted);
}
```

```
equals self: true
equals shifted: false
```

What these three operations share is that their results, on their own, expose no memory:
knowing whether an address equals itself differs from knowing the value at that address.
**LL1.** The `wrapping_add` call here was chosen deliberately — the language's pointer
method carrying this name returns a defined value even when the shift overflows, and for
that reason does not demand `unsafe`; by contrast, the `add` method that performs the same
shift asks the caller, as a precondition, that the result stay inside the same allocated
memory block or one byte past it, and because the compiler does not test this
precondition, calling it requires `unsafe`. The two methods do the same arithmetic; what
differs between them is what is guaranteed in the case of overflow.

By contrast, reading or writing a raw pointer and calling a function whose body is marked
`unsafe` both require an `unsafe` block — because both operations rest on an assumption
the compiler cannot test (that the address is valid, that the function's precondition is
met). The next two sections show this distinction by running code within the same block.

## Checking That Continues and Checking That Stops, in the Same Block

An `unsafe` block does not affect every line written inside it to the same degree. The run
below places two different operations inside the same block: one a direct read from a raw
pointer, the other a bounds-checked query on a slice. Both stand under the same `unsafe`
keyword, but only one is exempt from checking.

**LL2.** A four-element integer slice is used, and the queried index (the tenth item)
lies outside this slice's bounds.

```rust
// block_a.rs — checking that continues and checking that stops, in the same unsafe block
fn main() {
    let v = vec![10, 20, 30, 40];
    let pointer = v.as_ptr();

    // Two operations in the same block: one reads through a raw pointer (unchecked),
    // the other makes a safe slice query (v.get) (still checked).
    let (first_raw, tenth) = unsafe { (*pointer, v.get(10)) };

    println!("raw read: {}", first_raw);
    println!("tenth item: {:?}", tenth);
}
```

```
raw read: 10
tenth item: None
```

Both lines ran inside the same `unsafe` block, but they took different paths. `*pointer`
read the raw pointer directly — there is no bounds check here at all, whatever the address
points to is what gets read. `v.get(10)`, by contrast, passed through the slice's own
method; this method's body is not marked `unsafe`, and the bounds check inside it kept
running, which is why, asked for something past the fourth element, it returned `None`
instead of crashing. **LL3.** Here the `unsafe` block only lifted the obstacle in front of
the `*pointer` line; the `v.get` call was already a safe method, and its entering an
`unsafe` block did not change the checking inside its own body.

This observation is the lesson's central finding: `unsafe` is not a key, it does not
unlock all of the compiler's locks at once. The block grants permission only for the
specific operations written inside it — everything outside those operations, even inside
the block, stays subject to its own rules. The borrow checker is still in play here too:
`v`'s ownership, `pointer`'s having been derived from it, and both their scope rules — none
of this was suspended because of the `unsafe` block. The only thing suspended was the
bounds checking on the raw pointer read itself.

The run above deliberately showed this by placing the safe call inside the `unsafe` block;
in ordinary code there is no such requirement, `v.get(10)` does not require an `unsafe`
block on its own. Common practice is to keep an `unsafe` block limited to only the lines
that genuinely need it — the wider a block grows, the harder it becomes to tell afterward
which line rests on which contract. Writing two operations in the same block in this
lesson is meant to make concrete why that practice exists: even when a block is kept wide,
where checking continues and where it stops can be read from the source, but that
readability is not automatic — it requires tracing line by line.

## Three Delegated Operations

Like the `*pointer` read in the previous section, the lines where checking is delegated do
not stand alone — each of them can be placed side by side with a safe counterpart that
does the same job. The run below compares three separate delegated operations with their
safe counterparts: a read through raw pointer arithmetic, the slice's unchecked access
method, and a call to an `unsafe` function that carries a contract.

Though these three forms do the same job, where each is used differs. Raw pointer
arithmetic is used when a memory block needs to be walked independently of Rust's own
slice type — for instance, when reading a memory region returned from a foreign function, a
topic taken up in the next lesson. `get_unchecked` is used on a slice when the validity of
the index has already been proven somewhere else (by a loop bound, by a search result). A
contracted `unsafe` function is used to package these two forms together and present an
interface where it is enough for the caller to read only the signature — the contract is
still there, but its body can move to a different file.

**LL4.** The slice being compared is again `[10, 20, 30, 40]`; the raw pointer starts at
the second index and is shifted two positions further, the unchecked access goes straight
to the second index.

```rust
// block_b.rs — three delegated operations and their safe counterparts
unsafe fn get_second_item(v: &[i32]) -> i32 {
    // Contract: the caller guarantees that v carries at least two items.
    *v.get_unchecked(1)
}

fn main() {
    let v = vec![10, 20, 30, 40];
    let pointer = v.as_ptr();

    let raw = unsafe { *pointer.add(2) };
    let unchecked = unsafe { *v.get_unchecked(1) };
    let contracted = unsafe { get_second_item(&v) };

    println!("raw pointer read: {} (safe counterpart {})", raw, v[2]);
    println!("unchecked access: {} (safe counterpart {})", unchecked, v[1]);
    println!("contract-delegated function: {} (safe counterpart {})", contracted, v[1]);
}
```

```
raw pointer read: 30 (safe counterpart 30)
unchecked access: 20 (safe counterpart 20)
contract-delegated function: 20 (safe counterpart 20)
```

All three lines' results are identical to the bounds-checked access done with `[]`. This
is not a coincidence: the pointer advanced with `add(2)`, the `get_unchecked(1)` call, and
the call inside `get_second_item` all read the slice's memory layout correctly — only who
guarantees that correctness changes. In safe `[]` access, the guarantee is given by the
slice's own method, and it stops with a panic at run time if the index is out of bounds.
In `get_unchecked` and raw pointer arithmetic, nobody gives that guarantee; choosing the
correct index is the caller's obligation.

The `get_second_item` function exists to show a form that makes this obligation visible in
the source: the `unsafe` word in front of its signature says the body expects a
precondition to be met before it is called. This is a form the language itself recognizes
— the function's name and signature do not hide the unchecked access inside its body from
the caller's eye, they mark it instead. Marking a function `unsafe` also does not
automatically make every line inside its body unchecked: within the body too, only the
lines that genuinely require `unsafe` (here, the `get_unchecked` call) carry that status,
the rest is ordinary checked code. **LL5.** The party that calls the function as `unsafe`
has to verify for itself that `v` carries at least two elements; in this run, because the
slice carried four elements, the contract was met and the result matched its safe
counterpart.

There is a reason these three methods exist in the language: safe `[]` access performs one
bounds-checking step on every call — it tests whether the index is smaller than the
slice's length, then reads. `get_unchecked` skips that step; the only thing it reads is an
index the caller has already verified somewhere else. This lesson does not count that
step's cost, because the run is limited to a single access and duration is not measured
anywhere in this curriculum; what matters is that which check was skipped can be read from
the source — the name `get_unchecked` says directly, without hiding it, that the bounds
check was skipped.

## The Limiting Measurement

What these three operations share is that, used correctly, they leave no trace in the
result. In the run above, the raw read, the unchecked access, and the contracted call all
gave the same number as their safe counterparts — an observer looking only at the output
cannot tell which line was `unsafe`. The difference is not in the result, but in whose
responsibility it is what happens when they are used incorrectly.

Misuse is not run in this lesson, because a read that spills past the bounds is undefined
behaviour, and its result is specific to this machine, to this run — the next run might
not reproduce the same error, it might silently read a wrong value, or it might lead to a
crash. Describing it in prose is enough: a call to `pointer.add(100)` would try to read an
address that goes far past the four-element slice. The compiler does not reject this call
— because rejecting it would require knowing what the address points to, and it does not
know. In the same way, if `get_second_item` were called with a slice of fewer than two
elements, the contract would be violated, and the compiler would not notice this either;
the signature only assumes the caller has kept a promise, it does not test the promise.

This clarifies what `unsafe` checking does: the compiler no longer asks "is this correct",
it only asks "is this a form the source has permitted." Measuring where the boundary stops
is a separate matter from running what lies beyond it — this lesson does only the first of
those.

This distinction also explains why `unsafe` functions need a documentation convention.
Adding a documentation line above a function like `get_second_item` that spells out which
precondition is expected is not something the language requires — but it is a common
writing habit, because the `unsafe` word in the signature does not say by itself "which"
precondition, only that there **is** one. The function in this run carries that
documentation as a single-line comment; the Documentation lesson, later, will show that a
documentation line's own correctness is likewise not checked on its own, and falls into a
separate class (**nobody checks it**). What matters here is that `unsafe` itself does not
require this documentation, only that the language leaves a visible mark in the source:
without the `unsafe` word this function cannot be called at all, but the word's presence
does not show that the caller has actually passed a correct slice.

## Summary

- An `unsafe` block does not close off the compiler's entire checking; it only excludes a
  few specific operations (raw pointer read/write, calling an `unsafe` function) from
  scope — a safe call inside the same block (`v.get`) stayed in the **still holds** class
- A read through raw pointer arithmetic, a slice's unchecked access method, and a call to
  a contracted `unsafe` function — all three are operations where checking is **handed to
  the writer**
- All three delegated operations, used correctly, gave a result identical to their safe
  counterpart; the difference is not in the result but in the responsibility for misuse
- The borrow checker keeps working inside an `unsafe` block too — it is not a checker that
  closes, only a few operations that open
- The signature of a function marked `unsafe` is a form that makes the precondition inside
  its body visible to the caller; the compiler does not test this precondition, it only
  forces its existence to be declared

## Next Step

In this lesson, the delegated guarantee always stayed inside the same program — both the
raw pointer and the unchecked access were reading Rust's own memory layout; the compiler
always knew which type the pointer's target had and the slice's real length, it had only
stopped testing that knowledge at the moment of reading. The next lesson carries the same
delegation one step further: calling a function, written in another language, whose body
the compiler never sees at all. There, the contract is not only a precondition but a
signature — the compiler now compiles by trusting only the declaration written, without
ever looking at the other side's body.
