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>
This exercise has two functions. One does an operation with no checks and is
marked unsafe; the other checks its inputs first and is safe to call.
get_unchecked_sum adds up v[i] for every i in idx, reading each element
with slice::get_unchecked, which skips the bounds check. It is unsafe to
call because an index that is out of range makes it read memory outside the
slice. That is undefined behaviour (UB): the program has broken a rule the
compiler relies on, and Rust no longer promises anything about what happens.
checked_sum is the safe wrapper: it validates the indices, then calls
get_unchecked_sum. The exact contract:
vis any list ofi64values, possibly empty.idxis a list of non-negative indices, possibly empty, possibly with repeats.- Return
Noneifidxis empty, or if any index is>= v.len(). One bad index anywhere rejects the whole call. Whenvis empty, every index is out of range. - Otherwise return
Some(sum), where each index addsv[i]once per time it appears inidx. Use wrapping addition (i64::wrapping_add), so a sum that leaves thei64range wraps around in two's complement instead of overflowing. - The harness prints
{"value": n}forSome(n)and{"value": null}forNone.
For example, with v = [10, 20, 30, 40], idx = [0, 2] gives Some(40),
idx = [3, 3, 3] gives Some(120), and idx = [4] gives None because 4
is one past the last valid index.
The starter compiles, but the grade is compile, tests and
cargo clippy -- -D warnings, and the starter produces several warnings. The
first one is the point of the lesson: a warning with code E0133 inside
get_unchecked_sum. (The others are the unused parameters of the unfinished
checked_sum and a missing # Safety section, covered below.)
Two meanings of unsafe fn
Writing unsafe fn has always meant:
- The caller has obligations. Calling the function is an unsafe
operation, so the caller must use an
unsafe {}block and make sure the function's documented conditions hold. Otherwise the call may cause UB.
Before edition 2024 it also meant, by default:
- The whole body is an unsafe context. Inside an
unsafe fnyou could dereference raw pointers or call other unsafe functions without writing anyunsafe {}block.
The second meaning makes review harder. In a long unsafe fn, nothing marks
which lines perform unsafe operations, so a reader has to check every line to
find the ones whose correctness depends on the caller's promise.
RFC 2585 added the lint unsafe_op_in_unsafe_fn, which flags an unsafe
operation inside an unsafe fn that is not wrapped in its own unsafe {}
block. In editions 2015 to 2021 the lint is allowed (silent) by default. In
edition 2024 it is warn-by-default. With the lint in force, each unsafe
operation in the body gets its own block, as it would in a safe function:
/// # Safety
///
/// `p` must be non-null, aligned, and point to an initialised `i32`.
pub unsafe fn f(p: *const i32) -> i32 {
// SAFETY: the caller guarantees `p` is valid for reads.
unsafe { *p }
}
Without the inner block, edition 2024 reports
warning[E0133]: call to unsafe function ... is unsafe and requires unsafe block
(for get_unchecked), with the note "an unsafe function restricts its
caller, but its body is safe by default". It is a warning, not an error, so
the code still builds. Here the grade runs Clippy with -D warnings, which
turns every warning into an error, so the starter fails. (Outside this site
you can get the same effect for just this lint with
#![deny(unsafe_op_in_unsafe_fn)].)
Check your understanding
If unsafe operations in the body of an unsafe fn now need their
Show answer
Hide answer
own blocks, 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: "some
inputs make calling me undefined behaviour; read my # Safety section and
make sure you are not passing them."
When to mark a function unsafe
A useful rule of thumb:
Mark a function
unsafewhen there is some input, which safe code could pass, for which calling it causes undefined behaviour.
Containing unsafe code is not the test. slice::split_at_mut is implemented
with unsafe code, but it is a safe function: it checks mid <= len first and
panics otherwise. Every input either works or panics, and a panic is not UB.
(The rule of thumb has a known extension. A function can also be unsafe
because misusing it breaks an invariant that other unsafe code relies on, even
if the call itself does nothing wrong. Vec::set_len is the standard example:
setting a length larger than the initialised part does not read anything, but
later safe calls on the vector would.)
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 does
the same underlying operation but is safe, because it turns every bad input
into None before the unsafe call happens. The unsafe function states the
requirement; the safe function proves it is met.
The three Clippy lints in this exercise
clippy::missing_safety_doc is on by default. It warns about a public
unsafe fn whose doc comment has no # Safety section. The section should say
what the caller must guarantee (here: every element of idx is less than
v.len()), not describe what the function does internally.
clippy::unnecessary_safety_doc is off by default; the starter turns it on
with #![warn(...)] on its first line. It warns in the other direction: a
safe public function that has a # Safety section. Do not give
checked_sum one. Its callers have nothing to uphold, and a safety section
that asks for nothing makes real ones easier to ignore.
clippy::undocumented_unsafe_blocks, also turned on by the starter,
warns about an unsafe {} block with no // SAFETY: comment directly before
it (on the line above the block or above the statement containing it). That
includes the block inside checked_sum that calls get_unchecked_sum. The
comment is where you explain why the # Safety requirement holds at this
call. If you cannot write that explanation, the code probably has a bug.
Check your understanding
checked_sum calls an unsafe fn. Why is checked_sum itself
Show answer
Hide answer
not unsafe?
Because no input to checked_sum can cause UB.
Give it any Vec<i64> and any Vec<usize>: out-of-range indices, an empty
list, indices near usize::MAX. Each one either produces a sum or produces
None. The unsafe call only runs after the guard has checked every index.
This is what a safe abstraction means: unsafe code whose precondition is
enforced by a safe function around it, so safe callers cannot misuse it. Much
of the standard library, including split_at_mut, is built this way.
This argument works because the guard and the unsafe call are in the same function, so no other code can run between them. The article unsafe code and module invariants covers what changes when the check and the unsafe code are only in the same module, and why the whole module then needs auditing.
What the tests cannot check
Passing tests does not prove that code has no undefined behaviour. A tool such as Miri, which interprets the program and reports many kinds of UB, needs a nightly toolchain and is not run by this grader.
Suppose you wrote the guard with > instead of >=. Then idx = [4] with a
4-element v gets past the guard, and get_unchecked reads the 8 bytes just
after the end of the slice. The visible test for that input expects None, so
it would most likely fail, but only because the guard returned the wrong
variant. Nothing detects the out-of-bounds read itself. On a typical machine
it returns whatever bytes happen to be there, and since it is UB the optimiser
may assume it never happens, so the program can behave in other unexpected
ways too. A test suite only catches UB like this by luck, so check the guard by
reading it, not only by running it.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.