Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-434 · Case file with fixtures · Case 406 of 694 · Compiler evidence

An unsafe Trait Requires an unsafe impl Declaration

An unsafe trait moves a soundness obligation to every implementor. The unsafe impl keyword is the audit boundary, but correctness still requires a documented contract, controlled construction, and evidence that every implementation preserves it.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Safe consumers may rely on every implementation's extra-language guarantees, so the implementor must accept that proof obligation at an explicit unsafe impl boundary.
First discriminating check
Read the trait's Safety contract, audit every field, constructor, mutation, generic and auto-trait effect, then add unsafe impl only with a specific justification.

I declared a trait unsafe because safe consumers would rely on its implementors to preserve a low-level invariant. When I wrote a normal impl, Rust reported E0200 and required unsafe impl.

The failing fixture defines unsafe trait TrustedBytes and implements it for Packet without the keyword. The compiler explains that the trait enforces invariants it cannot check.

The keyword marks who owns the proof

An unsafe trait can have methods that are safe to call. Its danger lies in implementation: safe code may assume every implementor satisfies conditions beyond the type checker.

The Reference says the corresponding implementation must also begin with unsafe. This makes the audit boundary searchable and prevents accidental implementation through an ordinary blanket or derive-like pattern.

The keyword does not verify the proof. It says the author has taken responsibility for it.

Write the contract before the implementation

“Trusted bytes” is intentionally vague in the small fixture. A real unsafe trait must be exact. It may require stable layout, initialized bytes, thread-safe access, pinning, valid metadata, or a lifetime relationship.

I document:

  • the invariant every implementor must maintain;
  • which constructors or mutations can affect it;
  • what safe consumers are allowed to assume;
  • whether the guarantee is permanent or tied to a borrow;
  • examples of invalid implementations.

If I cannot state those points, I am not ready to expose the trait.

unsafe impl is not a repair by itself

The repaired fixture adds a safety comment and unsafe impl. Its tiny Packet([u8; 4]) representation makes the demonstration inspectable.

In production, I examine every field, generic parameter, auto trait, destructor, and mutation path. A one-line compiler fix can turn a build error into unsound code if the contract is not actually met.

Tests can find mistakes, but they cannot prove absence of undefined behaviour across all compiler optimisations and inputs. I combine narrow unsafe code, invariant reasoning, Miri where applicable, target tests, and review.

Send and Sync show the pattern

Send and Sync are unsafe traits because safe concurrency APIs rely on their implementations. An incorrect manual implementation can expose non-thread-safe state across threads even though downstream code contains no unsafe block.

The Nomicon chapter on Send and Sync demonstrates why implementation itself carries the obligation.

Most types receive correct automatic implementations based on fields. Manual unsafe impls deserve suspicion and a clear reason why structural inference is insufficient.

Keep construction aligned with the invariant

An unsafe trait implementation can be sound at first and become wrong when a public field or mutation method is added. I centralise construction and ensure every safe method preserves the declared property.

For generic wrappers, bounds often matter. unsafe impl<T> Trait for Wrapper<T> may promise too much if safety depends on T. The correct impl may require T: SomeSafetyTrait, ownership markers, or no implementation at all.

This connects to E0276 only superficially: unsafe trait bounds belong to the actual trait and impl design, while an ordinary impl still cannot narrow a safe callable method contract.

Call-site unsafe and implementation unsafe differ

An unsafe method requires callers to satisfy preconditions at each call. An unsafe trait requires implementors to establish type-wide guarantees so safe methods can rely on them.

A trait can contain unsafe methods without the whole trait being unsafe, and an unsafe trait can contain safe methods. I place responsibility on the party that controls the relevant fact.

Downstream implementation is part of the threat model

If the trait is public and not sealed, another crate may implement it for its own local type. The documentation must therefore be sufficient without access to private conversations or repository history. I include a dedicated Safety section and avoid depending on constructors that downstream implementors cannot use. When only my crate can uphold the invariant, sealing the trait can be more truthful than inviting unsafe external implementations.

My unsafe-impl checklist

  • What exact invariant does the trait documentation require?
  • Which safe code relies on it?
  • Does every field and generic argument preserve it?
  • Can safe constructors create an invalid state?
  • Can later mutation break the property?
  • Are Send, Sync, variance, and drop behaviour relevant?
  • Is the safety comment specific enough for another reviewer?
  • Can the abstraction remain safe instead?

The core principle is that an unsafe trait delegates part of Rust's soundness proof to implementors. unsafe impl makes acceptance explicit, but the real work is the invariant, its preservation, and its review. I add the keyword only after that evidence exists.