Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NoopLock is unsound #42

Closed
goffrie opened this issue May 12, 2020 · 7 comments
Closed

NoopLock is unsound #42

goffrie opened this issue May 12, 2020 · 7 comments

Comments

@goffrie
Copy link
Contributor

goffrie commented May 12, 2020

NoopLock implements RawMutex, which is an unsafe trait with the requirement

Implementations of this trait must ensure that the mutex is actually exclusive: a lock can't be acquired while the mutex is already locked.

However, NoopLock does not provide any such guarantees.

That means that a lock_api::Mutex<NoopLock, T> won't prevent multiple lock guards from being acquired simultaneously, which is unsound.

@Matthias247
Copy link
Owner

I was a little bit scared that exposing it would have a side effect when reviewing #38 .

I then decided it wasn't critical since usages like Channel require the RawMutex to be Sync in order for the created construct to be thread-safe:

impl<MutexType: RawMutex + Sync, T: Send, A> Sync for GenericChannel<MutexType, T, A>
where

So as long as you use NoopLock only with futures-intrusive it would be safe.
Passing a NoopLock to another library requiring a RawMutex might be an issue. But technically those should also require Sync on the RawMutex in order to to be able to use it for multi-threaded locking.

@goffrie
Copy link
Contributor Author

goffrie commented May 12, 2020

But you can also lock a mutex twice on the same thread and gain two simultaneous &mut references.

@goffrie
Copy link
Contributor Author

goffrie commented May 12, 2020

Sample:

type BadLock<T> = lock_api::Mutex<futures_intrusive::NoopLock, T>;
fn main() {
    let lock = BadLock::new(Some(String::from("hello world")));
    let g1 = lock.lock();
    let mut g2 = lock.lock();
    let string = g1.as_ref().unwrap();
    g2.take().unwrap();
    eprintln!("boom: {}", string.to_string());
}

@goffrie
Copy link
Contributor Author

goffrie commented May 12, 2020

(I actually think this is "exploitable" without #38 too, since you can use generics to unify a LocalMutex<T> with a GenericMutex<LockType, T> and then create your own lock_api::Mutex<LockType, T>, but that's a lot more contrived..)

@Matthias247
Copy link
Owner

I thought about this a bit more before pushing 0.4.

It seems like there doesn't exist any thread-safety issues, since lock-api still requires RawMutex to be Sync for that. See https://docs.rs/lock_api/0.4.2/src/lock_api/mutex.rs.html#140-141

With that in mind the borrow-checking problem persists. That could be fixed by converting NoopLock into a RefCell like implementation. A potential compromise could be to do that only in debug mode, and avoid the extra storage space in release mode. I would be open for a CR for that.

@goffrie
Copy link
Contributor Author

goffrie commented Dec 10, 2021

I thought about this issue again. NoopLock is also technically unsound even when used with futures-intrusive data structures like LocalMutex, because reentrancy is possible if a nonstandard Waker implementation is in use. This seems incredibly unlikely to happen in practice, but one could imagine e.g. a Waker with some instrumentation attached to it.

Here's a contrived example:

use std::cell::Cell;
use std::future::Future;
use std::ptr;
use std::rc::Rc;
use std::task::{Context, RawWaker, RawWakerVTable, Waker};

use futures::pin_mut;
use futures_intrusive::sync::LocalMutex as Mutex;

thread_local! {
    static MUTEX: Cell<Option<Rc<Mutex<()>>>> = Cell::new(None);
}

const VTABLE: RawWakerVTable = RawWakerVTable::new(
    |_| {
        MUTEX.with(|m| {
            if let Some(m) = m.take() {
                m.try_lock();
            }
        });
        RawWaker::new(ptr::null(), &VTABLE)
    },
    |_| (),
    |_| (),
    |_| (),
);

fn main() {
    let waker = RawWaker::new(ptr::null(), &VTABLE);
    let waker = unsafe { Waker::from_raw(waker) }; // safety: VTABLE meets the RawWaker invariants
    let mutex = Rc::new(Mutex::new((), true));
    let _lock = mutex.try_lock().unwrap();
    MUTEX.with(|m| {
        m.set(Some(mutex.clone()));
    });
    let f = mutex.lock();
    pin_mut!(f);
    let mut cx = Context::from_waker(&waker);
    assert!(f.as_mut().poll(&mut cx).is_pending());
}

This causes the MutexState to be locked twice simultaneously (you can tell because if you import the normal futures_intrusive::sync::Mutex instead, it deadlocks).

@Matthias247
Copy link
Owner

Since I had to push a new major revision anyway due to the parking lot version stuff I now simply made NoopLock back private.

For the last example: This deadlocks happens because it tries to lock the synchronous parking lot Mutex 2 times in the same thread. With the singlethreaded variant (NoopLock) this doesn't happen, and the try_lock will fail. The async Mutex is only allocated a single time and works as it should. Of course we discuss about the mutable reference things, but that seems the same discussion as #56.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants