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

RFA-433 · Case file with fixtures · Case 405 of 694 · Compiler evidence

Why an Implementation of a Safe Trait Cannot Be unsafe

unsafe impl acknowledges invariants declared by an unsafe trait; it is not a general marker for risky implementation code. Remove it for a safe trait, or make the trait unsafe only when safe consumers must rely on documented unchecked guarantees.

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
unsafe impl acknowledges invariants declared by an unsafe trait; it is not a general warning marker for an implementation body that happens to contain difficult or unsafe code.
First discriminating check
Remove unsafe for a safe trait and audit local unsafe blocks, or document a real soundness invariant before deliberately redesigning the trait as unsafe.

I saw unsafe operations in an implementation and wrote unsafe impl to signal extra care. Rust rejected it with E0199 because the trait itself was safe.

The failing fixture defines an empty safe trait named Ready and marks its implementation unsafe. The compiler says implementing Ready is not unsafe and suggests removing the keyword.

unsafe impl has a precise meaning

An unsafe trait declares that incorrect implementation can violate assumptions used by safe code. Marking its implementation unsafe is the implementor's acknowledgement that those documented obligations have been checked.

The Reference on unsafe traits requires implementations of unsafe traits to begin with unsafe. The relationship starts at the trait declaration.

A safe trait promises that any implementation expressible through normal Rust rules is acceptable to safe consumers. One impl cannot invent a private unsafe contract that callers do not know.

Remove unsafe for ordinary behavioural traits

The repaired fixture uses impl Ready for Job. That is correct when Ready is only a classification or behavioural interface and safe generic code does not use it to justify unchecked memory operations.

The implementation body may still contain an unsafe block. Each block needs its own proven preconditions, but that does not make the trait implementation declaration unsafe.

I keep these two questions separate:

  • Does this body perform an operation the compiler cannot verify?
  • Does every implementor owe an invariant on which safe consumers rely?

Only the second question concerns unsafe trait and unsafe impl.

Do not make a trait unsafe only to silence E0199

Changing trait Ready to unsafe trait Ready makes the fixture compile with unsafe impl, but it also changes the public contract. Every implementor must now audit some safety rule, and safe code may rely on it.

If no such invariant can be written precisely, the unsafe marker is misleading. It adds ceremony without locating responsibility.

Examples of legitimate unsafe traits include Send and Sync, where incorrect implementations can let safe code cross thread-safety boundaries unsoundly. Their guarantees are much stronger than “this implementation uses a raw pointer.”

Unsafe blocks stay small and local

Suppose Job wraps a foreign handle and calls FFI. The impl can remain safe while one method validates state and enters a small unsafe block. Callers use the safe trait normally because the implementation absorbs the proof obligation internally.

This is a central Rust pattern: build a safe abstraction around a reviewed unsafe core. Making the whole trait unsafe would push responsibility to implementors or callers unnecessarily.

Safety comments explain obligations, not emotions

I write a // SAFETY: comment beside an unsafe block or impl explaining which invariants are required and why they hold. “This looks safe” or “tested in production” is not sufficient.

For a trait, its documentation should state what implementors must guarantee, how long the guarantee lasts, and which safe operations rely on it. The impl comment then maps fields and constructors to that contract.

The Nomicon discussion of safe and unsafe helps separate operations requiring explicit proof from APIs that expose unchecked responsibilities.

Sealing can solve a different concern

Sometimes I do not trust downstream crates to implement a marker correctly, but an incorrect implementation would cause only logical errors, not memory unsafety. A sealed trait or private constructor can restrict implementations without declaring an unsafe contract.

Unsafe is not an access-control mechanism. It is about potential undefined behaviour through violated invariants.

Reviews need the trait and its consumers together

I do not review an unsafe marker in isolation. I search for generic code bounded by the trait and inspect every unsafe operation justified by that bound. If consumers never rely on an unchecked property, the trait probably remains safe. If they do, the contract must name the property before implementations can be judged. This direction from reliance back to obligation prevents decorative unsafe APIs.

My E0199 review

  • Is the trait declared safe?
  • Does safe generic code perform unchecked operations based on this trait?
  • Can the supposed invariant be documented and falsified?
  • Is risk local to an unsafe block inside one method?
  • Would sealing or module privacy address the real concern?
  • Are safety comments tied to concrete fields and constructors?

The core principle is that unsafe impl is an agreement with an unsafe trait, not a warning label for complicated code. For a safe trait, I remove it and audit unsafe operations locally. I make a trait unsafe only when implementors truly control invariants required for safe code to remain sound.