Safe Rust restricts operations so that ordinary callers do not have to prove pointer validity, aliasing rules or similar low-level conditions themselves. Unsafe Rust permits certain additional operations whose safety obligations the programmer must establish.
An unsafe block does not disable type checking or borrow checking. It marks
a place where the compiler permits operations such as raw-pointer
dereferencing without proving all their runtime preconditions. The program
must still obey those preconditions.
Safe Rust's guarantees depend on sound implementations of the abstractions it uses, including unsafe code in libraries and foreign code. A successful compilation is not a proof that every dependency is sound.
Undefined behavior
Undefined behavior, or UB, occurs when execution violates requirements that Rust relies on to define the program's behavior. Examples include using an invalid reference, reading uninitialized integer data or creating a data race. Each operation has its own requirements: merely forming a raw pointer is different from dereferencing it.
Once an execution has UB, the language does not guarantee a reasonable result. It might crash, appear to work or behave differently after an optimization or compiler change. A crash alone is not evidence of UB, and the absence of a crash is not evidence that an operation was valid.
Rust's detailed aliasing rules are still being developed. Use the documented contracts of the operations you call; avoid replacing them with rules such as "any pointer offset outside an allocation is always UB," which ignore differences between pointer APIs.
Check your understanding An out-of-bounds pointer read returned the expected value in a test. Does that make the read valid? Show answer Hide answer
No. That observation covers one execution of one build. It does not establish the pointer operation's preconditions. For a read, review allocation bounds, initialization, alignment, lifetime and the applicable access rules. Optimizers may assume that valid programs obey those requirements, so a later build need not preserve the observed result.
Sound and unsound APIs
A safe abstraction is sound when callers using its safe API cannot violate memory-safety requirements through it. This includes unexpected inputs and call sequences, not just the application's current happy path.
An unsafe function instead has explicit caller obligations. Its
implementation must work safely when those obligations hold. Document them
in a # Safety section; an unsafe signature does not excuse defects for
callers that satisfy the contract.
Consider a pointer-backed container whose safe indexing method constructs a
reference without checking the index. If a safe caller can supply an invalid
index, that interface is unsound. Checking the index can make failure return
None or panic. Another design is an unsafe unchecked method with a precise
caller contract. A safe slice API may remove the need for the raw operation.
A bounds check alone is not the entire proof. The container must also retain valid, initialized storage and return a reference with an appropriate lifetime and access permissions.
Privacy can limit who can break internal invariants, but it does not make unchecked code sound automatically. Audit all code that can modify the relevant state, including private safe methods. See unsafe code and module invariants.
Defined behavior can still be a bug
Memory safety is not a complete application specification:
| Behavior | Why it still needs review |
|---|---|
| Deadlock | Safe synchronization can leave tasks waiting indefinitely. |
| Memory leak | Safe code can retain allocations indefinitely, including through reference cycles or deliberate leaks. |
| Panic | Defined failure can still interrupt required work or expose a state-management bug. |
| Process abort | Terminates the process without running ordinary destructors; it is not orderly cleanup. |
| Incorrect output or file deletion | A memory-safe operation can violate the application's intended behavior. |
Integer overflow needs a more specific description than "debug panics, release wraps." For ordinary addition, subtraction and multiplication, overflow checking is configurable. Checked operations report failure; wrapping and saturating operations request their respective behavior. Signed minimum divided by negative one can panic even with overflow checks disabled. Unchecked arithmetic APIs have separate safety preconditions.
Unsafe abstractions must account for safe operations such as mem::forget.
They cannot rely on every destructor necessarily running. The
leak amplification exercise
explores this distinction.
Data races and timing-dependent logic
A data race involves conflicting accesses from different threads to the same memory, at least one write, with at least one non-atomic access and without the required synchronization. It is UB.
Safe sharing APIs use ownership rules, Send, Sync, atomics and locks to
provide valid ways to communicate. Some of that protection is checked
statically; synchronization can also have runtime costs. Unsafe
implementations must uphold the assumptions those safe APIs rely on.
A race condition is a broader logic problem in which timing affects whether the intended behavior holds. It can occur without a data race. For example, locking separately to check that a counter is positive and then to decrement it leaves a gap where another thread can change the counter. Each access can be synchronized while the combined operation is still wrong. Keep the check and update under the same lock when they must be one operation.
Review the invariant, not only the unsafe block
If a bounds check changes from index < len to index <= len, an unchanged
unchecked access below it may become invalid. The changed line can be safe
Rust syntax and still break the unsafe abstraction's proof.
Review initialization, mutation, error paths, panic paths and destruction together. A reference-count increment also needs an overflow strategy; a collection mutation needs a valid state if user code panics partway through. Which cases matter depends on the abstraction.
What the exercise harness can establish
Output tests and enabled lints can expose concrete bugs. They cannot prove the absence of UB for every input and call sequence. A latent soundness bug can pass the supplied cases.
Tools such as Miri can detect many invalid operations in executions they
cover, but they are not a universal proof either. This site's ordinary Rust
grading is not a Miri run. Likewise, -D warnings makes enabled warnings
fatal; it does not enable every optional unsafe-code lint.
Before an unsafe operation, state which preconditions are required and where the surrounding code establishes them. A useful safety comment explains those facts. Then use tests and analysis tools to challenge that reasoning, rather than treating the comment or passing tests as proof by themselves.