What is 'unsafe trait'?

What is 'unsafe trait'? Or when trait can be unsafe?
I saw the describe on Rust Docs: 'A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify.'. But I cannot understand it.
Please help.

You'll typically use unsafe traits when your code depends on the presence or correct implementation of the trait for safety.

For example, things like std::thread::spawn() rely on your type being Send to maintain thread safety. The Send trait doesn't actually have any unsafe methods or behaviour, but we still rely on its implementation being sound.

This is distinct from unsafe methods where the method may be doing unsafe things or make certain assumptions about the arguments.

It's when implementing that trait incorrectly could lead to undefined behavior such as memory unsafety, which are things that Rust normally prevents in code without unsafe.

2 Likes

unsafe trait means that unsafe code can rely on implementations of that trait to behave in a specific way, which is not true for non-unsafe traits.

// EXAMPLE

/// # Safety
///
/// this trait is safe to implement only if `core::mem::transmute::<Self, U>(..)` is safe
///
unsafe trait SafeTransmute<U> {}

// when implementing this trait we need to uphold all invariants of `SafeTransmute`

// SAFETY: we know that any `bool` is a valid `u8`
unsafe impl SafeTransmute<u8> for bool {}

fn safe_transmute<T, U>(t: T) -> U 
where
    T: SafeTransmute<U>,
{
    unsafe {
        // SAFETY: because `SafeTransmute` is an `unsafe trait` we can assume
        // based on `T: SafeTransmute<U>` that this transmute is safe.
        transmute(t)
    }
}
2 Likes

unsafe is about upholding guarantees that are necessary for memory safety without being able to fully rely on the compiler. With unsafe, there's always someone defining a contract, and someone who needs to uphold that contract. If the person who's supposed to uphold the contract fails to do so, the resulting program can turn out not to be memory-safe (e. g. do things like dereferencing an invalid pointer or double-freeing memory).

For an unsafe function/method, the person defining the function/method will define the contract, and the one calling the function has to follow that contract. For example the docs of slice::get_unchecked say that the index must be in-bounds, so when you're calling that function you must follow this rule set by the standard library authors who defined that method. The contract here is "the index must not be out-of-bounds for the slice". Like in this case, you'll find the "contract" for using an unsafe fn in the documentation of the function / method, conventionally in a section labeled # Safety.

An unsafe trait is similar, the situation there is as follows: The person that defines the trait defines the contract, and the person implementing the trait for some type must follow the contract. Notably, the person that needs to follow the contact is also someone who defines some methods, so the situation feels a bit different than with unsafe fn. Naturally, the contract for an unsafe trait should be in the documentation of the trait, conventionally in a section labeled # Safety.

For unsafe functions, the way to be explicit about "I'm promising to adhere to the relevant contract here" is to use an unsafe {} block. It's good style to leave a comment next to unsafe blocks explaining what the relevant contract was and how it's upheld. You can only call unsafe functions inside of unsafe blocks. For unsafe traits, the way to be explicit about "I'm promising to adhere to the relevant contract" is by writing an "unsafe impl" for the trait. You cannot implement an unsafe trait, without using the unsafe keyword on the impl. There, too, it's good style to leave a comment next to the unsafe impl explaining what contract is followed and how exactly it's followed.


The situation becomes a bit more complex / nuanced if you're having unsafe methods in a trait (that may itself be safe or unsafe); I haven't covered that point in this answer at all.

2 Likes

I am so sorry but this sentence make me more confused :sweat_smile:
I am very curious the "invariant" in that describe? Is there an example?

An example could be this trait:

/// Safety: The `get_an_index` method must return an index that is
/// within the bounds of the provided slice.
unsafe trait GetSliceIndex {
    fn get_an_index<T>(&self, slice: &[T]) -> Option<usize>;
}

This would make the unsafe block in this function correct:

fn get_the_value<T>(slice: &[T], getter: impl GetSliceIndex) -> Option<&T> {
    match getter.get_an_index(slice) {
        None => None,
        Some(idx) => unsafe {
            // Safety: The trait guarantees that the index is in bound.
            Some(slice.get_unchecked(idx))
        }
    }
}

Had the trait not been unsafe, then get_the_value would be incorrect because I would be able to implement GetSliceIndex for my own type and pass it to get_the_value, triggering undefined behavior without having written any unsafe in my own code.

In some sense it's about whose fault it is if things go wrong in unsafe code.

1 Like

In the std::thread::spawn() example we need to assume the Send implementation is sound.

An example of an unsound Send implementation would be unsafe impl<T> Send for Rc<T> { } because a Rc's reference count uses plain integer operations, but we need to use atomic integer operations to update it concurrently.

Invariant is just a fancy mathematical word for "something that is or must be always true".

4 Likes

I am confused on

trait GetSliceIndex {
    fn get_an_index<T>(&self, slice: &[T]) -> Option<usize>;
}

Why it is not

trait GetSliceIndex<T> {
    fn get_an_index(&self, slice: &[T]) -> Option<usize>;
}

Although I try to understand what you mean, I still have difficulty understanding when the unsafe trait must be used.
Is there such a situation that if use traits instead of unsafe traits, errors will occur during compilation?

Thanks a lot! Your code gave me great inspiration.

// EXAMPLE

/// # Safety
///
/// this trait is safe to implement only if `core::mem::transmute::<Self, U>(..)` is safe
///
unsafe trait SafeTransmute<U> {}

// when implementing this trait we need to uphold all invariants of `SafeTransmute`

// SAFETY: we know that any `bool` is a valid `u8`
unsafe impl SafeTransmute<u8> for bool {}

fn safe_transmute<T, U>(t: &T) -> &U where T: SafeTransmute<U> {
    unsafe {
        // SAFETY: because `SafeTransmute` is an `unsafe trait` we can assume
        // based on `T: SafeTransmute<U>` that this transmute is safe.
        &*(t as *const T as *const U)
    }
}

fn main() {
    let boolean = false;
    let byte = safe_transmute(&boolean);
    println!("`{}` as u8 = {}", boolean, byte);
}

One has to be more careful when transmuting references, regarding two factors:

  • alignment

    While it's safe to transmute types with different alignment directly, e. g. transmute [u8; 4] into u32 (and back) is sound, when transmuting references, alignment must not be increased (unless you add a runtime check for if the alignment of the reference just happens to be sufficiently higher than required, and fail with an error otherwise). So transmuting &[u8; 4] into &u32 is unsound because it will be UB if the &[u8; 4] does not happen to have a memory address that's divisible by 4. On the other hand, transmuting &u32 into &[u8; 4] is sound.

  • interior mutability

    While it can be safe to transmute types without interior mutability into types with interior mutability, e. g. transmuting T into Cell<T> is sound for any type T, but it is not sound to do this behind a shared reference. E. g. transmuting &u8 into &Cell<u8> will be unsound (either immediately UB, or at least once someone happens to mutate through the Cell, I'm not 100% certain which one).

If you want to support the function for transmuting references, like your code does, you'd need to change the safety requirements of the unsafe trait SafeTransmute to require not only mem::transmute::<Self, U> to be safe to call, but also mem::transmute::<&'a Self, &'a U> to be safe to call.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.