Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confusing safety contract of is_u8 #24

Closed
dtolnay opened this issue Mar 18, 2021 · 9 comments · Fixed by #26
Closed

Confusing safety contract of is_u8 #24

dtolnay opened this issue Mar 18, 2021 · 9 comments · Fixed by #26

Comments

@dtolnay
Copy link
Contributor

dtolnay commented Mar 18, 2021

BorshSerialize and BorshDeserialize have an is_u8 trait method which is unsafe to call (but safe to implement). What contract is the caller required to uphold when calling is_u8 in order for the call to be sound?

@dtolnay
Copy link
Contributor Author

dtolnay commented Mar 18, 2021

In other words the UB repros in #17 and #18 both continue to be an issue.

@evgenykuzyakov
Copy link
Contributor

@dtolnay

What do you mean it's safe to implement? Does Rust allows to implement a trait method that is marked unsafe without unsafe?

Also if you have suggestion how to the handle it better, I'd prefer to do this. The issue is we want to implement Borsh serialization and deserialization of Vec<u8> and &[u8] differently from default implementation of Vec<T> and &[T] where T: BorshSerialize or T: BorshDeserialize

@dtolnay
Copy link
Contributor Author

dtolnay commented Mar 18, 2021

BorshSerialize and BorshDeserialize are safe traits -- by language semantics that means it is unsound for the memory safety / thread safety of the program to hinge on either of them being implemented "correctly". This is 100% orthogonal to whether an individual trait method is safe or unsafe to call (which is what unsafe fn means). A trait that is unsafe to implement can contain methods that are safe to call, and a trait that is safe to implement can contain methods that are unsafe to call.

Reference:

https://doc.rust-lang.org/1.50.0/reference/items/traits.html#unsafe-traits
https://doc.rust-lang.org/1.50.0/nomicon/safe-unsafe-meaning.html
https://doc.rust-lang.org/1.50.0/book/ch19-01-unsafe-rust.html
https://doc.rust-lang.org/1.50.0/std/keyword.unsafe.html

matklad added a commit to matklad/borsh-rs that referenced this issue Mar 19, 2021
matklad added a commit to matklad/borsh-rs that referenced this issue Mar 19, 2021
@matklad
Copy link
Contributor

matklad commented Mar 19, 2021

Yeah, that's all true. I believe there's no clean way to express this in Rust (what we ideally want is an unsafe super-trait, but that requires specialization, bringing us back to the square 1).

#25 implements a simpler solution of just hiding the method from crate's public API.

@RalfJung
Copy link

I believe there's no clean way to express this in Rust

unsafe trait sounds like a clean way to express "the user must be careful when implementing this trait (e.g., by not overwriting a certain method)".

@cramertj
Copy link

One pattern that can be used in order to have a safe trait rely on guarantees is the following:

struct Guarantee { _private: () }

impl Guarantee {
   // Safety: only call if ...
   pub unsafe fn promise() -> Self { Guarantee { _private: () } }
}

trait RequiresGuarantee {
   fn check_guarantee(&self) -> Guarantee;
}

Then users can call check_guarantee, which, if it returns without diverging, must have internally called promise using an unsafe block, so the requirements on promise must be satisfied.

@matklad
Copy link
Contributor

matklad commented Apr 21, 2021

To be clear, the underlying issue was solved way nicer by dtolnay here by just not using is_u8 at all.

@RalfJung there's a twist here, in that we want to (unsafely) override this for u8, and, for all other types, use default (safe) impl. Or, as I've said, we could use unsafe trait, but that needs specialization.

@cramertj that won't work exactly as spelled -- the user user can get Guarantee without unsafe by calling RequiresGuarantee::check_guarantee on some other type that implements it. You need to make check_guarantee unsafe to call as well:

// You write code like
struct CramertjType;

impl RequiresGuarantee for CramertjType {
    fn check_guarantee(&self) -> Guarantee { 
      // Carefully verify invariants
      unsafe { Guarantee::promise() } 
    } 
}

// I write code like
struct MatkladType;

impl RequiresGuarantee for MatkladType {
    fn check_guarantee(&self) -> Guarantee { 
      // Life's too short for verifying invariants, lets use someone else's promise
      CramertjType::check_guarantee()
    } 
}

@cramertj
Copy link

cramertj commented Apr 21, 2021 via email

@RalfJung
Copy link

there's a twist here, in that we want to (unsafely) override this for u8, and, for all other types, use default (safe) impl. Or, as I've said, we could use unsafe trait, but that needs specialization.

You could use unsafe trait without specialization, but I guess that's not as ergonomic as you'd like it to be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants