dirtbiker
dirtbiker

Reputation: 51

How to reconcile Rust Mutex and (C) caller supplied locking mechanisms?

I'm writing a PKCS#11 library in Rust and I'm running into some trouble with how to reconcile caller supplied locking mechanisms i.e. CreateMutex, DestroyMutex, LockMutex, UnlockMutex with Rust's implementation of mutexes.

Mutexes in C, are of course, not tied to data. They simply set a lock over a section and the programmer is responsible for the creation/destruction and locking/unlocking of the mutexes. Whereas, Rust does tie data to the mutex itself.

How can I reconcile both cases: when supplied locking mechanisms from the calling program (not tied to data and manually create/lock/unlock/destroy) vs no locking mechanisms are supplied and to use Rust locking (tied to data and MutexGuard and scoped dropping/unlocking of mutex)?

Should I use parking_lot::Mutex with the raw_lock/raw_unlock to make the patterns similar e.g. not tied to data?

Upvotes: 4

Views: 338

Answers (1)

Aiden4
Aiden4

Reputation: 2654

This is my solution; it's not pretty but it works and passes the miri test. The general idea is we bundle a Mutex<()> and an optional MutexGuard for that mutex. I did not add proper handling of errors or naming to match the standard.

The structs:

struct MutexContainer {
    mutex: Mutex<()>,
    guard: Cell<Option<MutexGuard<'static, ()>>>,
}
pub struct OpaqueMutex {
    _data: [usize;0],
}

Note that OpaqueMutex is simply an opaque handle to avoid leaking the internals of the MutexContainer, as is best practice during ffi. The key thing with this design is that the struct cannot move if it holds a guard.

Mutex creation:

pub unsafe extern "C" fn create_mutex(mutex: *mut *mut OpaqueMutex) -> libc::c_ulong {
    *mutex = Box::into_raw(Box::new(
        MutexContainer {
            mutex: Mutex::new(()),
            guard: Cell::new(None),
        },
    ))
    .cast();
    0
}

Nothing fancy going on here, just leaking the mutex on the heap.

Mutex locking:

pub unsafe extern "C" fn lock_mutex(mutex: *mut OpaqueMutex) -> libc::c_ulong {
    let container:&MutexContainer = &*mutex.cast();
    let lock:MutexGuard<'static, _> = mem::transmute(container.mutex.lock().unwrap());
    container.guard.set(Some(lock));
    0
}

Here I use mem::transmute to lengthen the lifetime of the lock and store the result. The transmute is safe because we ensure that we don't move its parent mutex and we don't let it outlive the mutex here or elsewhere. The mutex will remain locked until the guard is dropped.

Mutex unlocking:

pub unsafe extern "C" fn unlock_mutex(mutex: *mut OpaqueMutex) -> c_ulong{
    let container:&MutexContainer = &*mutex.cast();
    if container.mutex.try_lock().is_ok(){
        return !0; //can't unlock an unlocked mutex
    } else {
        container.guard.set(None);
    }
    0
}

Here we use the nonblocking try_lock to determine if the mutex is locked. If try_lock succeeds that means the mutex is unlocked and we return an error. Otherwise, we drop the guard by overwriting it with None.

Mutex drop:

pub unsafe extern "C" fn drop_mutex(mutex: *mut OpaqueMutex) -> c_ulong{
    unlock_mutex(mutex);
    drop(Box::from_raw(mutex.cast::<MutexContainer>()));
    0
}

Here we ensure the mutex is unlocked to prevent a dangling reference by the guard and then reclaim the memory by reboxing the pointer.

Full playground link plus test harness.

Disclaimer: while this code passes miri, it is far from production-ready: there is basically no error handling. You should make sure that errors are handled robustly and sanity checks (like null and alignment checks) are made, especially since you are implementing a crypto library.

edit: the example main program invoked undefined behavior by erroneously locking the mutex outside of the new threads unlocking them inside the threads, rather than locking the mutex within each thread and unlocking them when done. Not sure why I thought that was a good idea.

Upvotes: 1

Related Questions