0

According to The Rust Programming Language Chapter 19.1, Safe traits impose no safety requirement on their implementors. More quite literally, it says:

A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify.

Well, before long rationale, simply put: does the phrase "some invariant" include safety condition for calls? Let me rephrase. Assume the documentation of an unsafe method f in a safe trait T has a "Safety" section that says "the call of f is safe if and only if Condition A is satisfied." Then could it constrain us to implement the method f so that it is sound to call as long as Condition A is satisfied, even though the trait T is safe?

[EDIT: added paragraph] To make things clear, let me quote my rephrase from my comment. The trait Eq requires (x == x) == true, but implementors don't need to obey it for safety. I wrote below an example safe trait Foo with an unsafe method foo. The trait Foo requires that self.foo(x) is sound provided x: u32 is even, but implementors don't need to obey it for safety...!? Or do they? That's my question. [/EDIT]

I have long thought unsafe methods of safe traits can't have any trustworthy safety invariant. At least, that's what our TRPL says. Even if we are given a trait like the following:

pub trait Foo {
    /// # Safety
    /// This is safe to call if and only if `x` is even. 
    unsafe fn foo(&self, x: u32);
} 

I thought the following impl would be valid, because, after all, Foo is safe to implement:

#![deny(unsafe_op_in_unsafe_fn)]

use core::hint::unreachable_unchecked;

pub struct Bar;

impl Foo for Bar {
  unsafe fn foo(&self, _: u32) {
    unsafe { 
      unreachable_unchecked();
    }
  }
}

and that the following code would be unsound:

fn gen_call_foo<T: Foo>(t: &T) {
  unsafe {
     t.foo(2);
  }
}

I guessed gen_call_foo should be marked unsafe and should pass the callers of gen_call_foo the obligation to check the soundness of concrete implementations <SomeStruct as Foo>::foo. Because, I imagined, that's how safe traits work! Maybe Foo's documentation is misleading and should receive rewording. It could say "Since this is a safe trait, this method lacks any safety guarantee for call. If you would like to wrap a call to this function in a safe code (with an unsafe block), you should instantiate the concrete implementation and consult the implementation's document. Nonetheless, the implementors are strongly recommended to make this safe to call where x is even".

However, I found two pieces of information on the Web that made me doubt my belief. The first one is an (unstable) safe trait Step. It has forward_unchecked. It is an unsafe method, and its document innocently says

It is undefined behavior for this operation to overflow the range of values supported by Self. If you cannot guarantee this will not overflow, use forward or forward_checked instead.

as if it's safe to call when no overflow occurs! It doesn't even give an notice about the "fact" that since this is a safe trait, no call of this unsafe method can be guaranteed to be safe unless you check individual implementations. I started to suspect I had misunderstood something (well, as I'm writing this, I'm beginning to suspect Step's safety condition is maybe only a tautology, i.e. the term overflow could be defined to be the situation where a call to this function is unsound. I guess this question is worth clarifying anyway).

Also, we have comments for this answer to this question on this site ("When is it appropriate to mark a trait as unsafe, as opposed to marking all the functions in the trait as unsafe?"). I just quote the comments:

Since an unsafe trait and unsafe functions in a trait are orthogonal, can you think of any cases where you might have both? – Shepmaster Jul 26, 2015 at 13:12

@Shepmaster: I can't think of a practical example, but it's certainly not impossible to envisage an unsafe trait where a subset of its methods have an unsafe interface. Perhaps a marker trait (like Send or Sync) where you have unsafe code relying on the marker trait which also exposes a method dealing in raw pointers (which would thus be unsafe to call). – DK. Jul 26, 2015 at 13:17

This astonished me, for I had thought unsafe methods are one of the main reasons of marking a trait unsafe, for we want trait methods to be at least conditionally safe to call generically.

My belief is getting more and more fragile. Maybe, even with unsafe methods, Rust's default is safety. Probably, we can only conditionally lift the safety of the function by explicitly mentioning in the documentation. Safe traits impose no restrictions on implementations, but maybe that doesn't apply to the soundness of the function calls: the Safety section in the function's documentation say something. After all, we always assume unconditional safety of the calls to safe functions even in safe traits, and it poses no restriction on safe implementations.

However, I couldn't find a documentation for that. I'm at a loss.

I wrap this up and I state the question again. If the document of an unsafe function in a safe trait has some safety condition, can callers of this function trust it? Must implementors obey it for soundness? Additionally: if this question has a definite answer, a document for that answer would be greatly appreciated.

gksato
  • 368
  • 1
  • 10
  • 1
    When a function or method is marked as `unsafe` it means _the caller_ must uphold some invariant(s) that the compiler can't check. When a trait is marked as `unsafe` it means _the implementer_ of that trait must uphold some invariant(s). Since those invariants, by definition, can't be statically verified by the compiler, you can only trust them as far as you trust the author. Your implementation of `Foo` for `Bar` is unsound since it's UB to ever reach a `unreachable_unchecked` call (as per the docs). – isaactfa Oct 24 '22 at 15:10
  • @isaactfa "When a function or method is marked as unsafe it means the caller must uphold some invariant(s) that the compiler can't check." Yes, the safety invariant `fn is_call_of_foo_is_safe(t: &Foo, x: u32) -> bool` for the `impl Foo for Bar` is `fn is_call_of_foo_is_safe(_: &Foo, _: u32) -> bool { false }`. – gksato Oct 24 '22 at 15:12
  • As an extension to what isaactfa said, `gen_call_foo` as implemented is sound because it upholds the safety invariant of `Foo::foo`, namely `x` is even. Again, it is your implementation that is unsound. You should not have to look at the implementations to know if calling an `unsafe` method on a trait is safe or not. – kmdreko Oct 24 '22 at 15:15
  • @kmdreko "Safety invariant" of `Foo::foo` is written in a *safe* trait, which can impose *no* restriction on the implementation. That's what I mean. If a safe trait `Eq` says "the implementation should ensure `(x == x) == True`", implementations don't need to obey it for safety. If a safe trait `Foo` says "the implementation should ensure the call `self.foo(x: u32)` is safe if x is even", implementations don't need to obey it for safety. "Or do they?" that's my question. – gksato Oct 24 '22 at 15:27
  • @gksato if you follow what you're saying to its logical conclusion, `unsafe` trait methods would be useless because you'd *never* be able to guarantee calling it is sound. – kmdreko Oct 24 '22 at 15:50
  • @kmdreko Yes, that's what I mean. So I was astonished to see the current `Step` trait because it was safe and had unsafe methods. – gksato Oct 24 '22 at 15:52
  • I thought the only use case would be `trait Unreachable { unsafe fn unreachable(self) -> !; }` with `struct Checked; impl Unreachable for Checked { fn unreachable(self) -> ! { unreachable!(); } }` and `struct Unchecked; impl Unreachable for Unchecked { unsafe fn unreachable(self) -> ! { unreachable_unchecked(); } }`. – gksato Oct 24 '22 at 15:54

1 Answers1

2

A trait being unsafe means there are invariants that need to be upheld by the implementor, while a function being unsafe means there are invariants that need to be upheld by the caller. Consider the following trait:

/// A trait to abstract over array-like containers. 
/// For any in-bounds index, `std::ptr::eq(self.get(index), unsafe{ self.get_unchecked(index) }` must be true. 
trait Arraylike<T> {
    ///returns the length of the container
    fn len(&self) -> usize;
    /// returns a reference to the element at index, panicking if the index isn't less than `self.len()` 
    fn get(&self, index: usize) -> &T;
    /// returns a reference to the element at index
    ///# Safety
    /// index must be less than `self.len()`
    unsafe fn get_unchecked(&self, index: usize) -> &T;
}

By making get_unchecked unsafe, a well-behaved implementation could use the invariant to elide a bound check and potentially speed things up. Whoever calls get_unchecked must be aware of the potential for undefined behavior, an make sure to do the bounds check themselves. However, an implementation must still ensure that get_unchecked doesn't cause undefined behavior if it is called with an index less than self.len(), regardless of how that implementation is defined.

In contrast, since the trait isn't unsafe, the trait level invariant saying that get and get_unchecked must return the same value can't be relied upon for a program's soundness. An implementation that decided get should be zero indexed and get_unchecked should be one indexed is perfectly valid, so long as the value of self.len() is large enough, and any programs making use of Arraylike in a generic context need to be aware of this, and can't have their code's soundness be reliant upon the implementation being correct. Making Arraylike an unsafe trait would lift that restriction, allowing users of the trait to rely on the implementation being correct for program soundness.

Aiden4
  • 2,504
  • 1
  • 7
  • 24
  • "an implementation must still ensure that get_unchecked doesn't cause undefined behavior if it is called with an index less than self.len(), regardless of how that implementation is defined." does not belong to "invariants that need to be upheld by the implementor"? – gksato Oct 24 '22 at 17:34
  • 2
    @gksato not more than the requirement that safe functions can't cause undefined behavior is. From a safety standpoint, an unsafe function you've verified the documented prerequisites for is the same as a safe function. The only difference between an unsafe trait function and a regular unsafe function is who gets to define those perquisites. For the former, it's the creator of the trait, while the latter is the creator of the function. – Aiden4 Oct 24 '22 at 17:51
  • it looks like you're answering what I wanted to ask, but I failed to parse the first sentence. You mean that anything not more than the requirement that safe functions can't cause undefined behavior does not belong to "invariants that need to be upheld by the implementer" that enforces traits to be unsafe? – gksato Oct 24 '22 at 18:08
  • 1
    @gksato sorry, that was poorly worded on my part. What I meant was that a safe function in a trait isn't allowed to cause undefined behavior, and an unsafe function in that trait also isn't allowed to cause undefined behavior if the necessary prerequisites, as defined by the documentation, to call it are met. – Aiden4 Oct 24 '22 at 20:15
  • Ah, now I parsed your sentence right ;) "It does not more than the requirement (that ...) does." Anyway, this is the exact thing I wanted to ask. Would you mind adding your comment in your answer before I accept it? Also, equally importantly, is there explicit documentation for the fact that conditional Safety guarantee for call of unsafe method in a trait documentation does not constitute a safety constraint on implementation that turns a trait unsafe (though it IS a safety constraint on implementation without that-clause)? – gksato Oct 25 '22 at 02:49