Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-520 · Case file with fixtures · Case 492 of 694 · Compiler evidence

Rust Inherent Impl Blocks Cannot Be Marked Unsafe

Unsafe belongs on operations and contracts that require extra proof, not on a container for inherent methods. Mark individual functions unsafe when callers must uphold invariants.

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 marks a proof boundary on operations or trait contracts, while an inherent impl is only a container for associated items with no shared safety assertion.
First discriminating check
Remove unsafe from the impl and place precise caller obligations on unsafe functions or justified operations inside local unsafe blocks.

I wrote unsafe impl Counter around ordinary inherent methods. Rust emitted E0197 because an inherent impl does not implement an unsafe trait and cannot itself be marked unsafe.

The failing fixture is useful when code contains pointer operations and someone tries to label the whole method collection as dangerous. Rust asks for a more precise boundary.

An inherent impl is only an item container

impl Counter { ... } associates methods, functions, and constants with the local type. It does not make one contract assertion comparable to implementing an unsafe trait.

The official E0197 page says inherent implementations are always written without unsafe. Removing the keyword restores the correct structural form.

Safety obligations still exist inside or around particular items, where Rust can represent them directly.

The repaired fixture keeps safe behaviour safe

The repaired fixture uses a normal impl and exposes value(&self). Reading a field has no caller-side safety precondition, so the method remains safe.

Marking a larger scope unsafe would not make this method more correct. It would only blur which operation needs review.

I keep the safe surface as large as soundness permits and unsafe surfaces as narrow as necessary.

Unsafe functions place obligations on callers

If an inherent method dereferences a raw pointer under conditions it cannot verify, it may be declared unsafe fn. Its safety documentation must state the conditions a caller must uphold.

The body still needs explicit unsafe operations according to current unsafe-operation rules. An unsafe function does not turn every body expression into unreviewed code.

I pair each unsafe function with a safe wrapper when validation can be centralised.

Unsafe blocks justify local operations

A safe method can contain an unsafe block when the method establishes all required invariants internally. Callers then receive a safe abstraction and do not inherit the proof burden.

I put a safety comment next to the block explaining pointer validity, alignment, initialization, aliasing, or FFI assumptions. The comment should connect concrete checks and type invariants to the operation.

The Reference on unsafe distinguishes these contexts.

Unsafe trait impls are different

An unsafe trait declares that incorrect implementation may violate safety even when users call safe code. Implementing it requires unsafe impl Trait for Type and a proof of the trait's documented invariants.

That syntax includes a trait name and for. An inherent impl has only the target type. Copying unsafe from one form to the other causes E0197.

I verify whether the code implements a trait before interpreting the keyword.

Generated code should retain safety at item granularity

A generator may receive one “unsafe” flag for a group and place it before impl. This is too coarse for inherent APIs. The schema should record unsafe functions, unsafe blocks, and unsafe trait implementations separately.

Compile tests need both safe and unsafe methods in one impl, proving the generator does not contaminate the whole container. Review tools can then count actual unsafe operations rather than misleading blocks.

Audit from public calls toward unsafe operations

When reviewing a low-level type, I start from each public safe method and follow its path to every unsafe block. For each block I ask which earlier validation proves its requirements and whether mutation can invalidate that proof before use. This is more useful than marking a whole file or impl “unsafe code.”

I also enable unsafe_op_in_unsafe_fn policy so unsafe functions still show their concrete operations. That keeps a caller-facing contract distinct from an implementation-side justification. A method can be unsafe without currently executing a raw operation, and a safe method can legitimately contain one after proving its invariants. The two axes should remain visible in code review and generated documentation.

My E0197 checklist

  • Is this an inherent impl or a trait impl?
  • Which exact operation carries a safety precondition?
  • Should the method be unsafe for callers or safe with an internal unsafe block?
  • Is there an unsafe trait whose implementation requires proof?
  • Does safety documentation name every invariant?
  • Can validation move into a safe wrapper?
  • Did generated code put one coarse flag on the impl?
  • Does the repaired API keep ordinary methods safely callable?

The core principle is that unsafe is a proof boundary, not a warning sticker. I attach it to the function, block, or trait implementation where the extra invariant actually lives.