We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 3 of 24
`unsafe {}` vs `unsafe fn`: edition 2024 splits the two meanings
pub unsafe fn get_unchecked_sum(v: &[i64], idx: &[usize]) -> i64
pub fn checked_sum(v: Vec<i64>, idx: Vec<usize>) -> Option<i64>
get_unchecked_sum sums v[i] for every i in idx, using
slice::get_unchecked — no bounds checks at all. It is unsafe to call,
because an out-of-range index reads memory the slice does not own.
checked_sum is the safe wrapper: it validates, then calls. It returns
None if idx is empty or if any index is >= v.len(), and Some(sum)
otherwise. Use wrapping addition so a large input cannot overflow.
The starter compiles under rustc but fails the gate. Read the diagnostic
carefully — it is E0133 and it is the whole lesson.
Two jobs that used to share one keyword
Before RFC 2585, unsafe fn did two completely unrelated things at once:
- it declared that the caller has obligations — “you must uphold X or this is UB”;
-
it made the entire body an implicit
unsafeblock.
Job 2 was a disaster in practice. A 200-line unsafe fn had no marker
anywhere saying which three of those lines were the dangerous ones. Every line
was equally unmarked, so nothing stood out, so nothing got audited.
Edition 2024 splits them. unsafe fn now does only job 1. The body is safe
Rust by default, and each unsafe operation inside it needs its own unsafe {}
block, exactly as it would in any other function:
pub unsafe fn f(p: *const i32) -> i32 {
// SAFETY: the caller guarantees `p` is valid for reads.
unsafe { *p }
}
Mechanically, this is the lint unsafe_op_in_unsafe_fn. It is
warn-by-default in edition 2024, so the code still builds — but because
the grade here is clippy -D warnings, a warning is a failure. The diagnostic
carries the code E0133 and the note “an unsafe function restricts its
caller, but its body is safe by default”.
::: question If the body of an unsafe fn is now ordinary safe Rust, what does
the unsafe on the function signature still mean?
It means calling this function is an unsafe operation, so callers need
their own unsafe {} block and their own justification.
It is a statement about the contract, not about the contents. The keyword
now says one thing only: “there exist inputs, reachable from safe code, for
which calling me is undefined behaviour — read my # Safety section and prove
you are not passing them.”
:::
The rule for when to mark a function unsafe
This is the single most useful heuristic in the topic, and it is not the one beginners guess.
Mark a function
unsafeif and only if there exists an input, reachable from safe code, for which calling it is undefined behaviour.
Not “because it contains unsafe code”. slice::split_at_mut is full of unsafe
code and is a perfectly safe function, because it checks mid <= len first and
panics otherwise — every possible input either works or panics, and a panic is
not UB.
That is the shape you are building here. get_unchecked_sum is unsafe
because passing idx = [99] with a 3-element v is UB. checked_sum takes
the same underlying operation and makes it safe by converting every bad
input into None before the unsafe call happens. One function owns the danger;
the other owns the proof.
Three lints are watching, and each teaches a different half
clippy::missing_safety_doc (on by default) will reject a pub unsafe fn
with no # Safety section in its doc comment. Write a real one. It must say
what the caller must guarantee — “every element of idx must be less than
v.len()“ — not what the function does internally. A # Safety section is a
contract offered to a stranger.
clippy::unnecessary_safety_doc is enabled by the template, and it fires
in the other direction: a safe function with a # Safety section. Do not
put one on checked_sum. There is nothing for its caller to uphold, and
claiming otherwise trains readers to skim safety docs, which is worse than
having none.
clippy::undocumented_unsafe_blocks, also enabled, wants a // SAFETY:
comment on the line immediately before every unsafe {} — including the one
inside checked_sum where you call get_unchecked_sum. That comment is where
you discharge the obligation the # Safety section stated. If you find
yourself unable to write it, that is the signal that the code is wrong.
::: question checked_sum calls an unsafe fn. Why is checked_sum itself
not unsafe?
Because no input to checked_sum can cause UB.
Feed it any Vec<i64> and any Vec<usize> you like — garbage indices, an
empty list, indices larger than usize::MAX / 2. Every one of them either
produces a sum or produces None. There is no reachable input for which the
unsafe call happens with a bad index, because the guard runs first.
That is what “safe abstraction” means, and it is the entire business of this
track: unsafe code with an enforced precondition wrapped in a safe signature.
The standard library is thousands of instances of this pattern.
Note the corollary though — it is only true because the guard and the unsafe call are in the same function, so nothing can get between them. Item 17.20 is about what happens when they are merely in the same module, and why the answer is “you must audit the whole module”. :::
What the harness cannot check
Honesty, and this recurs on every page in this track: the grader cannot prove your code has no undefined behaviour. Miri needs a nightly toolchain and a cargo project and cannot run here.
If you write checked_sum with > instead of >= in the bounds test, every
test case in this problem still passes — the off-by-one only reads one i64
past the end of the allocation, which on any real machine reads some adjacent
heap byte and returns a number. It is undefined behaviour, the optimiser is
entitled to assume it never happens, and nothing here will tell you. Read your
guard twice.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.