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

RFA-648 · Case file with fixtures · Case 620 of 694 · Compiler evidence

A Pointer Cast Cannot Add Send to an Erased Trait Object

Send and Sync are semantic implementation promises, not metadata bits to attach after erasure. Preserve required auto traits when constructing the object and across every API boundary.

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
A semantic thread-transfer guarantee was erased and later treated as a pointer metadata bit that could be restored without concrete type evidence.
First discriminating check
Require Send when coercing the known concrete value and preserve it through every registry, pointer, and task boundary.

Send means ownership of a value may safely move to another thread. Once a concrete type has been erased as plain dyn Any, a raw pointer cast cannot establish that promise. The failing fixture tries to add Send to the object type and receives E0804.

Auto traits describe the concrete implementation

Most types receive Send and Sync automatically from their components, while raw pointers, Rc, and thread-affine resources can prevent them. The property is about behaviour and ownership of the concrete value, not only pointer size.

The official E0804 explanation warns that adding an auto trait through a pointer cast may make vtable assumptions invalid and enable undefined behaviour. The standard library defines the unsafe marker trait Send.

I require the bound at the moment a known concrete value is coerced into the trait object, while rustc can still prove it.

Preserve the bound from construction

The repaired fixture creates *const (dyn Any + Send) directly from &(), whose concrete type satisfies the bound. No after-the-fact strengthening is needed.

Owned registries use Box<dyn Plugin + Send> when values move between threads and add Sync when shared references may cross threads. An Arc does not make its contents thread-safe by itself; Arc<T> is Send/Sync only under the relevant T bounds.

If an API erases to Box<dyn Plugin> too early, downstream code cannot safely recover the lost guarantee. I push required auto traits into the public boundary and reject incompatible implementations at compilation.

Removing a bound and adding one are asymmetric

Coercing dyn Trait + Send to dyn Trait forgets a capability and is often permitted in supported pointer contexts. Going the other direction would invent evidence. This resembles converting a broad lifetime to a narrower use versus claiming borrowed data is static.

Downcasting through Any can recover a known concrete type and then re-establish bounds in typed code, but it requires knowing which type to request. It is not a generic cast between trait-object capability sets.

I do not follow a compiler suggestion to use transmute unless I have an exceptional unsafe proof covering concrete type, metadata, provenance, lifetime, and all later safe exposure. In ordinary architecture it signals the bound was lost at the wrong layer.

Send is not Sync and neither means race-free logic

Send permits ownership transfer. Sync permits shared references to cross threads. A type can implement one without the other. Interior mutability must provide correct synchronization for Sync.

These traits prevent classes of memory unsafety, not application races, duplicate messages, or invalid state transitions. A Send service can still require single-threaded ordering at the protocol level.

The Reference summarises auto traits and restrictions on explicit implementations. Unsafe manual impls require a complete invariant over all fields and future methods.

Async executors surface the same boundary

A multi-threaded executor often requires spawned futures to be Send because they may resume on another worker. A non-Send value held across await makes the future non-Send. Casting an erased pointer cannot fix that.

I shorten the value’s scope before await, use a local executor for intentional thread affinity, or choose a thread-safe owner. Each repair changes scheduling or ownership and should be explicit.

Compile tests assert Send/Sync at component boundaries and include implementations which intentionally fail. Runtime concurrency tests then cover ordering, cancellation, and shutdown properties beyond the marker traits.

My E0804 checklist

  • Is a pointer cast attempting to add Send, Sync, or another auto trait?
  • What concrete type was erased, and where could its capability have been proven?
  • Should the registry or constructor require the auto trait from the start?
  • Did an early API erase bounds needed by a later thread or task boundary?
  • Is the intended operation ownership transfer, shared access, or thread-affine use?
  • Can scope reduction or a local executor preserve a valid non-Send design?
  • Is unsafe transmutation being proposed without a complete metadata and type proof?
  • Do compile assertions and runtime tests cover both marker and protocol guarantees?

The core principle is that auto traits are evidence about erased concrete behaviour. E0804 prevents a cast from fabricating that evidence. I preserve required capabilities during construction and keep thread-affine values inside boundaries that can honour them.