Borrowed reference into owned


#1

Here is the silly Rust problem I’m having. Consider it a Rust exercise :slight_smile:

Imagine, you get a reference to &A, but need to somehow construct a reference &(A, B).

Roughly, the reason I need this are the following:

  1. (A, B) is an associated type Context for an implementation of a particular trait from a 3rd-party crate (so, it’s outside of my control).
  2. My type have to have 'static lifetime, so that Context associated type cannot have lifetime parameters.
  3. That Context associated type is always passed via reference to that 3rd-party crate, which in turn will pass it back to one of the functions on that trait, implemented by me. So, internally I always get &(A, B).

Let’s say, A is not cloneable. But you can create a “default” one.

So, one option would be to create a default A, then swap A I get it into the tuple, call the code requiring tuple, then swap it back. That would require &mut A reference instead of &A, but that’s ok. Hacky, but would work.

Now, let’s say I have to have &A. However, I don’t really need A as part of the tuple, I need something that will deref to A. But without lifetime parameters.

What I was thinking, is doing something like that:

pub struct Ref<T> {
  ptr: std::ptr::NonNull<T>,
}

impl <T> std::ops::Deref for Ref<T> {
  type Target = T;
  fn deref(&self) -> &T {
    unsafe {
      self.ptr.as_ref()
    }
  }
}

fn own_ref<T, R, F>(borrowed: &T, callback: F) -> R where F: FnOnce(Ref<T>) -> (Ref<T>, R) {
  let owned = Ref {
    ptr: borrowed.into(),
  };
  callback(owned).1
}

And my tuple will be (Ref<A>, B).

The own_ref function would give Ref to the callback with the “promise” to get it back (via return value). Once we get our Ref back, we can be sure there are no references to borrowed anymore.

However, this implementation is not safe. It can be trivially abused by calling own_ref two times, then swapping outer Ref with inner Ref, causing a read against dangling pointer.

The question though, would it be possible to make it safe?

Assuming that we don’t need Send/Sync, I can add a flag to the struct to mark it as “erased” after the call: flag: std::rc::Rc<std::cell::Cell<bool>> (I guess then I don’t really need to ask for it back – I would simply “defuse” Ref by setting flag to false and make it panic on deref).

Would it make it safe?

Am I missing a simpler solution?


#2

Why taking a reference in the first place, instead of a moved object? If you can’t clone it, the caller should be responsible for constructing an object. Maybe a CoW will suit your needings?


#3

This is not possible. &(A, B) means that A and B are laid out in a continuous memory region, as specified by (A, B) layout. You can’t construct &(A, B) from two unrelated A and B unless you combine them into a (A, B) first.

Rc satisfies this requirement.


#4

You might be able to use a bit of carefully crafted unsafe code to get a &(A, B) given only an &A - @dtolnay has an example at https://stackoverflow.com/a/46044391.

But, I’d only resort to this after all safe options were exhausted.


#5

Why taking a reference in the first place, instead of a moved object?

Caller, for whatever reason, wants to pass &A. Primarily, because in 99% of the rest of the code, this worked just fine… Until I got to refactor this little piece of code :slight_smile:

Rc satisfies this requirement.

It would, if I could change caller to pass be Rc<A>.

You might be able to use a bit of carefully crafted unsafe code to get a &(A, B) given only an &A

Oh, that’s a good trick! It will break if A have an internal mutability, though?


#6

I don’t think it should “break” due to internal mutability, but perhaps I’m not understanding the type of break you’re concerned with.


#7

I don’t think it should “break” due to internal mutability, but perhaps I’m not understanding the type of break you’re concerned with.

I was thinking if it would be possible to violate Rust memory safety by using the fact you are given a “unauthorized” copy. But maybe not. Things like RefCell would not work as expected, but wouldn’t cause memory issues.

Although, hold on. The ManuallyDrop protection would be ineffective if you have something like RefCell<Option<Other>> inside A – you can .take() it out and drop. What if this Other is an Rc?


#8

Types with interior mutability already allow (by their definition/purpose) aliasing, but see below …

ManuallyDrop in that SO example is to ensure the “fake” String doesn’t drop memory it thinks it owns (but doesn’t). In your example, it seems true that wrapping A in ManuallyDrop doesn’t help if the caller can then extract an Rc out that’s been created out of “thin air” - you may need to add the appropriate protections elsewhere, if that’s possible. So yeah, this approach may not work as simply (if at all) as in that SO scenario.

I think the feasibility of this approach is highly-dependent on what exactly you’d be exposing to the caller, and thus what actions they could take. If they only get immutable references to things, then you ought to be fine. If they get owned values or mutable references, it gets trickier.


#9

So yeah, this approach may not work as simply (if at all) as in that SO scenario.

Even in SO scenario, it is not safe unless you control both types A and B, see the code. I can drop strong count of Rc to zero.

So, going back to my original attempt, I tried to make it (maybe) safe by using a flag. The question I have, though, is it still possible to misuse it in unsafe manner (assuming I don’t control the type T and the code inside the callback)?

The insight I have is that deref only checks when called, so if you get that reference while it is safe and use it later, it will explode. However, I cannot think of the ways how to transfer that reference past the callback scope?

One idea I have is maybe use a “rental” crate to make a struct which is “owned: Ref, derefd: &'owned T”, then put this struct whether you want (because it will be 'static) and finally use it later to make everything go in flames? :slight_smile:

I guess, “erasing” lifetimes might not be such a great idea, after all :slight_smile: