Course contentsShow
← Rust
Lesson 1135 of 1,91010. Concurrency PrimitivesFree lesson

The Dangers of Unsound Send Implementation

You will diagnose and fix memory corruption caused by incorrectly marking non-thread-safe types as Send.

In this lesson, you will learn why manually implementing Send on a type that isn't inherently thread-safe is a critical violation of Rust’s safety guarantees.

The Promise of Send

The Send trait is a "marker trait"—it contains no methods. It acts as a contract between you and the compiler, promising that it is safe to transfer ownership of a value across thread boundaries. When you use standard types like Vec<T> or Box<T>, Rust automatically determines if they are Send. However, when dealing with raw pointers or FFI, you might be tempted to manually implement Send to silence compiler errors. This is dangerous.

The Anatomy of an Unsound Implementation

Consider a type that wraps a non-thread-safe resource, like a Rc<T>. Rc (Reference Counted) pointers use non-atomic increments. If you move an Rc to another thread, two threads might try to update the same counter simultaneously, leading to a data race and memory corruption.

If you force this to compile by lying to the compiler, disaster strikes:

use std::rc::Rc;

struct UnsafeWrapper(Rc<i32>);

// DANGER: We are lying to the compiler!
unsafe impl Send for UnsafeWrapper {}

fn main() {
    let wrapper = UnsafeWrapper(Rc::new(10));
    
    std::thread::spawn(move || {
        // The compiler lets us move this here, 
        // but Rc is not designed for concurrent access!
        let _val = wrapper.0; 
    });
}

By manually implementing Send, you are essentially opting out of the compiler's primary safety check. If the internal state of your type depends on the thread that created it—such as thread-local storage, non-atomic reference counters, or certain GUI context handles—moving it to another thread will cause "undefined behavior." This might manifest as subtle, intermittent crashes or silent data corruption that is nearly impossible to debug.

How to Fix It

If your type needs to be shared across threads, do not force Send. Instead, use synchronization primitives that are designed for concurrency. Replace Rc with Arc (Atomic Reference Counted) and RefCell with Mutex or RwLock. These types provide the necessary atomic operations to ensure that data access remains safe across thread boundaries, making manual Send implementations unnecessary and unsafe.

Key Takeaway: Never manually implement Send unless you are 100% certain the underlying data handles concurrent access synchronization; if you have to lie to the compiler, your design is already broken.