Are `mem::{replace, swap, take}` unsound?

These, however, require the user to either have another owned instance or for the type to implement Default, respectively. In @Mokuz' most recent example¹, only &mut Tokens are ever let outside the privacy barrier so it's impossible to call these functions— I believe this is roughly the same principle that Pin uses to prevent !Unpin types from being moved.

This doesn't implicate std::mem::* in any way, but the take_mut crate will let you temporarily get an owned Token from this &mut Token. Either the unsafe block in Token::cleanup() or the one in take_mut::take() must necessarily be unsound, but it's unclear to me which one.


¹

2 Likes

I am curious: can you implement replace, &c. in terms of other standard library functions? Much like you can leak objects with channels or reference cycles, so having those things in safe Rust forces a safe leak function. It seems like mem::swap should exist, and that gives you the other functions. Is there anything else apparently weaker that lets you move out of a mutable reference?

It's not possible to have a safe interface to get T from &mut T which is weaker than mem::replace (without T: Copy or the use of a closure with an unwind to abort guard like take_mut). This is because unless you put some value back into the place, you're exposing access to a moved-from value in that place.

The closest to an API codifying the ability would be &mut T -> &mut ManuallyDrop<T>, but taking the value would still necessarily be unsafe, so this isn't much different from just pointer casting.

The closest to an acknowledgement that this is how API soundness functions is the Pin API. You can't assume that a T's address is stable when handling out &mut T to untrusted code, and need to use Pin<&mut T> (with T: !Unpin) instead, which prevents access to &mut T (which thus presumably confers the power to change the pointee's address). On the other hand, Pin<&T> always just gives you access to plain &T (which thus presumably does not confer the power to change the pointee's address[1]).


  1. Thus an API which makes a temporary copy of the T behind &T and exposes &T to that temporary copy would not be sound to expose on arbitrary types without specific unsafe opt in. This is fairly easily evident because of UnsafeCell, but it's not immediately obvious that a Freeze bound (autotrait unimplemented for UnsafeCell) isn't sufficient either, as there could be code relying on address stability not because of shared mutability. (Although a realistic example not just relying on an unsafe assume equivalent is somewhat difficult to construct and likely to run afoul of Stacked Borrows' tight provenance. It'd need to either be based on a table of addresses somewhere else, or accessing memory beyond the statically sized portion, which under SB can itself be thought of as such a table, just implicitly via exposed addresses.) ↩︎

3 Likes

Is there a safe interface which is equivalent, but not obviously so?

Equivalent to take_mut? Not as part of std. I don't think std has any instances of unwind-to-abort shims (other than nounwind shims for #[global_allocator] and (soon) extern "C"), and I think we should avoid adding APIs which use them (since an unwind aborting is an undesirable footgun to run into). As mentioned, the take_mut API shape fundamentally requires an unwind shim.

The closest safe equivalent is the "DerefMove" capability of Box, in that you can move out of a box and leave it uninitialized, as well as potentially move back into the box and recomplete it. Borrowck enforces that this usage is correct and the box is recompleted before it's used again. This essentially works by treating the box pointee the same as another local binding w.r.t. borrowck and dropck.

This could theoretically be allowed for &mut T, which would allow writing mem::swap with just the obvious safe code. However, this would still be extremely limited for the same reason of unwinding, as nearly every expression could potentially unwind, and the language is not going to change what code is considered valid based on whether panics are configured to unwind or to abort. (The exceptions are basically just built-in place evaluation and assignment.)

The next closest would just be (the obviously unsafe) ptr::read and its documentation on ownership of the returned value.

2 Likes

To be sound, if you take a value from behind an &'a mut T, you need to put back another valid T before the end of 'a to prevent use-after-free¹.

So any potentially-equivalent safe interface will need some way to get its hands on an owned T. For swap, it comes from another &mut T; for replace it's passed in directly; for take it comes from calling T::default()— Any other way of producing the T that will end up behind the reference when the borrow is concluded could potentially be the basis of a swap or replace alternative.

take_mut does this by calling a transforming closure fn(T)->T to produce the replacement value, but needs machinery to protect against non-local returns, such as panics.


¹ Alternatively, ensure that the program terminates before 'a ends (effectively making 'a equivalent to 'static).