Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is mutable accessor using a cast safe?

Tags:

rust

unsafe

I am trying to understand the problems with duplicated code for & and &mut in getter-type functions. I'm trying to understand whether a particular solution to this problem using a cast inside an unsafe block would be safe.

The following is an example of the problem. It is taken from the really nice tutorial Learning Rust With Entirely Too Many Linked Lists.

type Link<T> = Option<Box<Node<T>>>;

pub struct List<T> {
    head: Link<T>,
}

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    // Other methods left out...

    // The implementation of peek is simple, but still long enough
    // that you'd like to avoid duplicating it if that is possible.
    // Some other getter-type functions could be much more complex
    // so that you'd want to avoid duplication even more.
    pub fn peek(&self) -> Option<&T> {
        self.head.as_ref().map(|node| {
            &node.elem
        })
    }

    // Exact duplicate of `peek`, except for the types
    pub fn peek_mut(&mut self) -> Option<&mut T> {
        self.head.as_mut().map(|node| {
            &mut node.elem
        })
    }
}

The solution

It seems to me that you can use a cast in an unsafe block to solve this problem. The solution seems to have the following properties:

  • It can be done in a safe way.
  • It provides a safe interface for an unsafe implementation.
  • The implementation is simple.
  • It removes the code duplication.

The following is the solution:

// Implemention of peek_mut by casting return value of `peek()`
pub fn peek_mut(&mut self) -> Option<&mut T> {
    unsafe {
        std::mem::transmute(self.peek())
    }
}

These are the arguments for why I think it seems safe:

  1. The return value of peek() is from a known source with a known alias situation.
  2. Since the parameter type is &mut self there are no refs to its elements.
  3. Hence the return value of peek() in unaliased.
  4. The return value of peek() does not escape this function body.
  5. Casting an unaliased & to &mut doesn't seem to violate the pointer aliasing rules.
  6. The lifetimes of the target and source of the cast match each other.

Misc notes

  • The same problem is discussed the following question: How to avoid writing duplicate accessor functions for mutable and immutable references in Rust?

    This question is different because it asks about details of one specific solution to the problem.

  • There are other kinds of solutions to the problem, such as this one: Abstracting over mutability in Rust

    But all other solutions seem to introduce quite a bit of extra complexity to the code.

  • The Nomicon asserts strongly that it is always undefined behaviour to cast & to &mut, which implies that this solution is not safe. But it makes no attempt whatsoever to explain why.

  • The Rust Reference states that "Breaking the pointer aliasing rules" is UB. To me it seems like this solution doesn't do that, for reasons given above.


Questions

I have the following questions to people with deeper Rust knowledge than myself:

  • Is the provided implementation of peek_mut safe?
  • Are there other necessary arguments that are needed to establish that it is safe, that I have missed?
  • If it is indeed not safe, why is that? Can you give a detailed explanation?
  • Is there in that case a similar solution to the problem that is safe?
like image 212
Lii Avatar asked Jan 04 '20 21:01

Lii


1 Answers

I believe this code will invoke undefined behaviour. To quote the Nomicon:

  • Transmuting an & to &mut is UB
    • Transmuting an & to &mut is always UB
    • No you can't do it
    • No you're not special

More to the point, the Rust compiler will mark the return value of peek() as immutable in the LLVM intermediate representation, and LLVM is free to make optimisations based on this assertion. It might not currently happen in this specific case, but I still think it's undefined behaviour. If you want to avoid the duplication at all costs, you can use a macro.

like image 97
Sven Marnach Avatar answered Oct 04 '22 16:10

Sven Marnach