Search code examples
asynchronousrustself-referencerust-pin

Why doesn't my self-referential struct work (invalid memory reference)?


I am trying to implement a self-referential struct in rust.

Background

I am writing a crate which is based on another crate. The underlying crate exposes a type Transaction<'a> which requires a mutable reference to a Client object:

struct Client;

struct Transaction<'a> {
    client: &'a mut Client
}

The only way to construct a Transaction is by calling a method on a client object. The function looks something like this:

impl Client {
    async fn transaction<'a>(&'a mut self) -> Transaction<'a> {
        // ...
    }
}

Now I want to expose an api that allows something like this:

let transaction = Transaction::new().await;

Of course, this doesn't work without passing a &mut Client.

Now my idea was to create a wrapper type which owns a Client as well as a Transaction with a reference to said client.

I would use unsafe to create a mutable reference which can be used to create a Transaction while still being able to pass the Client object to my struct. I would pin the Client on the heap to make sure the reference doesn't become invalid:

struct MyTransaction<'a> {
    client: Pin<Box<Client>>,
    inner: Transaction<'a>
}

impl<'a> MyTransaction<'a> {
    async fn new() -> MyTransaction<'a> {
        // ... fetch a new client object
        let pin = Box::pin(client);
        let pointer = &*pin as *const Client as *mut Client;
        
        // convert raw pointer to mutable reference 
        // to create new Transaction
        let inner = unsafe {
            &mut *pointer
        }.transaction().await;

        Self {
            inner,
            client
        }
    }
}

When I run this code however, my test fails with the following error message:

error: test failed

Caused by:
  process didn't exit successfully: `/path/to/test` (signal: 11, SIGSEGV: invalid memory reference)

This somewhat surprised me because I thought that by pinning the client object I'd ensure it won't be moved. Thus, I reasoned, a pointer/reference to it shouldn't become invalid as long as it doesn't outlive the client. I though that this could not be the case since the client is only dropped when MyTransaction is dropped, which will also drop the Transaction which holds the reference. I also though that moving MyTransaction should be possible.

What did I do wrong? Thanks in advance!


Solution

  • The problem here is the implicit drop order placed on the fields, in Rust that order is the declaration order meaning your client gets dropped before inner so when inner is dropped and uses it's reference to client that reference is dangling and causes the SIGSEGV you see. A simple fix is to just reorder them:

    struct MyTransaction<'a> {
        inner: Transaction<'a>
        client: Pin<Box<Client>>,
    }
    

    For more complex scenarios see Forcing the order in which struct fields are dropped