Some unsafe Rust tricks

I'm doing a hobby project in Rust and I have used a couple of unsafe tricks that I'd like to share.

Trick #1: Lifetime transmute

Let's imagine the following:

  • we have a struct that is shareable across threads
  • some of struct's fields are protected by a mutex
  • we want to lock the mutex and send the lock guard across a channel

Normally, a lock guard has a lifetime parameter (e.g. struct MutexGuard<'a, T> with lifetime parameter 'a) that is used to guarantee that the lock guard cannot outlive the lock. The way this guarantee is enforced prevents us from sending an owned lock guard across a channel.

Here's an example of a compilation error:

error[E0597]: `buf` does not live long enough
  --> src/main.rs:16:17
   |
13 |     let buf = Arc::new(Buffer {
   |         --- binding `buf` declared here
...
16 |     let guard = buf.data.blocking_lock();
   |                 ^^^ borrowed value does not live long enough
...
19 |         guard,
   |         ----- this usage requires that `buf` is borrowed for `'static`
20 |     });
21 | }
   | - `buf` dropped here while still borrowed

for the following code:

fn producer(tx: Sender<LockedBuffer>) {
    let buf = Arc::new(Buffer {
        data: Mutex::new(vec![1,2,3]),
    });
    let guard = buf.data.blocking_lock();
    tx.send(LockedBuffer {
        buf: buf.clone(),
        guard,
    });
}

struct LockedBuffer {
    buf: Arc<Buffer>,
    guard: MutexGuard<'static, Vec<u8>>,
}

struct Buffer {
    data: Mutex<Vec<u8>>,
    // ....
}

(full code)

The borrow check is preventing us from doing something potentially unsafe, as it does not understand that we're packing buf and guard into a single struct, and guard references inside buf. Rust's borrow check is just a local analysis, so it's natural that it fails to see that.

We can easily work around by extending the lifetime to 'static with std::mem::transmute:

    let guard = buf.data.blocking_lock();
    
    // SAFETY: the `guard`'s referent `buf` is kept alive for the lifetime of `guard`,
    // because it's stored in `LockedBuffer`, and `LockedBuffer` is not destructured.
    let guard: MutexGuard<'static, _> = unsafe { std::mem::transmute(guard) };

    tx.send(LockedBuffer {
        buf: buf.clone(),
        guard,
    });

(full code)

The trick works because Rust uses lifetimes only for borrow checking, and lifetimes are completely erased after borrow-checking. That means that as long as we don't anything bad at runtime, having a “wrong” lifetime does no harm.

In general, transmute can be dangerous, but in this case its use is sound, since we take additional measures to ensure that the lifetime constrains between a lock and its guard are satisfied.

Note that there is a yoke crate that uses a similar technique (but is much more advanced and safe).

Trick #2: controlling access to shared data with external locks

Normally, Mutexes and RwLocks in Rust act as containers that wrap the protected resource. This design leads to an ergonomic and safe API, but can be restrictive at times.

I have a case where I have a node-based data structure and I want to use a single lock to control access to set of fields of all nodes instead of having a mutex inside of each node.

This access pattern can be implemented with UnsafeCell, which requires some unsafe code around it. In this specific case, the use of UnsafeCell is pretty hard to make safe in general. But it is still useful and can be sound.

It works like this:

  • the owning struct has a lock (which will be used to control access to child nodes). If we don't have any other state to protect, we can just add a empty unique type.
    struct Buffers {
        lock: Mutex<BuffersLockToken>,
        buffers: Vec<Buffer>,
    }
    struct BuffersLockToken;
    
  • the internal struct has an UnsafeCell around the protected data:
    struct Buffer {
        flags: UnsafeCell<u32>,
    }
    
  • we also need to implement Sync for this struct (by default, UnsafeCell is not shareable across threads):
    unsafe impl Sync for Buffer {}
    
  • we provide method to access the resource. The method access a reference to the lock guard for the owner to serve as an evidence that that lock is held. This method must be unsafe as it's possible to call this method multiple times on the same instance and obtain multiple mutable references (which is UB in Rust).
    impl Buffer {
        // SAFETY: `flags_mut` should not be called multiple times on the same `Buffer` with the same `lock_evidence`
        unsafe fn flags_mut<'a>(&'a self, _lock_evidence: &'a MutexGuard<'a, BuffersLockToken>) -> &'a mut u32 {
            // SAFETY: the access is protected by the lock on owning `Buffers`
            // this is not completely safe, but safe enough.
            unsafe {
                &mut *self.flags.get()
            }
        }
    }
    
  • and it's used like so:
    let buffers = Buffers { ... };
    let guard = buffers.lock.blocking_lock();
    *buffers.buffers[0].flags_mut(&guard) = 123;
    

(full code)

Even though this usage requires unsafe at the call site (as the user of flags_mut must ensure some of the safety invariants), it is still totally worth it in my project.