Safe Send Wrapper Validation in Rust: Ensuring Thread Safety for Structs with Conditional Non-Send Fields

mod send_safe {
    pub struct SendSafe<T>(T);
    pub trait ISendSafe {
        fn check(&self) -> Option<String>;
        fn into_safe(self) -> SendSafe<Self>
        where
            Self: Sized,
        {
            SendSafe::from(self)
        }
    }
    impl<T: ISendSafe> SendSafe<T> {
        pub fn from(t: T) -> SendSafe<T> {
            if let Some(msg) = t.check() {
                panic!("{}", msg);
            }
            Self(t)
        }
        pub fn into_raw(self) -> T {
            self.0
        }
    }
    unsafe impl<T> Send for SendSafe<T> {}
}

type NotSend = Rc<i32>;
struct A {
    map: HashMap<NotSend, NotSend>,
    rc: Rc<NotSend>,
    option: Option<NotSend>,
}
impl ISendSafe for A {
    fn check(&self) -> Option<String> {
        if !self.map.is_empty() || Rc::strong_count(&self.rc) > 1 || self.option.is_some() {
            Some("not safe".to_string())
        } else {
            None
        }
    }
}

#[test]
fn test_unsafe_send() {
    let a = A { map: Default::default(), rc: Rc::new(Rc::new(1)), option: None };
    let send_a = a.into_safe();

    std::thread::spawn(move || {
        drop(send_a);
    }).join().unwrap();
}

I"m implementing a safety wrapper to allow sending structs containing non-Send types when specific conditions are met. The requirements are that all non-Send fields must be either:

  1. Empty containers (like HashMap)
  2. None values in Options
  3. Owning exclusive ownership (reference count = 1)

The implementation checks these conditions in the check() method before constructing the SendSafe wrapper. Could this approach have hidden safety issues? Specifically:

  • Are there any edge cases I might be missing with container types?
  • Does the current reference count check properly ensure exclusive ownership?
  • Are there any potential race conditions despite these checks?
  • Does the unsafe Send implementation follow Rust"s safety requirements?

This is probably a better fit for https://users.rust-lang.org/ than this forum.


The idea isn’t bad though. I’ve realized myself before that for a number of !Send types, they can actually be in a Send-safe state. The execution looks a bit unidiomatic (e.g. I-prefix on a trait; using String for error messages), and isn’t quite correctly following unsafe convensions / there are some soundness bugs/holes.

On the latter point:

  • mainly, you’d need to make your SendSafe-related trait actually an unsafe trait, because impls of this trait do assert memory-safety relevant correctness
    • in particular, the check must not return None unless the value is actually in a state that’s safe for crossing thread boundaries
  • additionally, the example around Rc isn’t sound, because a check on the strong-count alone ignores the possibility of weak references
    • with reference-counted smart pointers, an existing API to compare to could be the “Unique-” variant of the Arc from triomphe, which features a impl<…> Send for …<T> requiring just T: Send, not T: Send + Sync
    • your execution on Rc is wrong also in that you’re allowing Rc<NotSend>, even though the only thing a unique reference can give you is the same Send-safety contract as Box does, i.e. Send if and only if the contained value is Send. Which is a relaxation over what Rc and Arc do, that is, Rc is never Send, and Arc requires T: Send + Sync (the +Sync thus being extra).

I haven’t thoroughly reviewed your API, this is just the things I’m noticing on first read through, but I may likely be missing some things/details.

1 Like

To be honest, if I were going to do this today, I would probably do it by making a helper type that’s always-Send, and then providing a conditional conversion to and from that type when going in and out of your box. Then the compiler can still help you. But maybe you’ve got perf concerns or pinning requirements that make that infeasible.

trait SometimesSend {
  type SendableForm: Send;
  fn from_sendable_form(value: SendableForm) -> Self;
  fn try_into_sendable_form(self) -> Result<SendableForm, Self>;
}
3 Likes