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

Wrong atomic ordering in Drop impl for RwLockWriteGuard and buggy lock acquisition failure in RwLock::try_read #65

Closed
64 opened this issue Jun 27, 2019 · 4 comments · Fixed by #66

Comments

@64
Copy link
Contributor

64 commented Jun 27, 2019

Multiple issues in the RwLock implementation.

The first (and the worst) one is here:

impl<'rwlock, T: ?Sized> Drop for RwLockWriteGuard<'rwlock, T> {
    fn drop(&mut self) {
        self.lock.store(0, Ordering::Relaxed);
    }
}

Use of Ordering::Relaxed is incorrect. The compiler is free to reorder a write behind this, which could lead to two mutable refs being used at the same time (UB). It should be Ordering::Release instead (maybe this was just a typo?). Other parts of this code use the needlessly strict SeqCst ordering, but that's not a soundness issue.

The second that I've found is a bug in the try_read function:

    pub fn try_read(&self) -> Option<RwLockReadGuard<T>>
    {
        let old = (!USIZE_MSB) & self.lock.load(Ordering::Relaxed);
		// readers variable could be incremented here
        let new = old + 1;
        if self.lock.compare_and_swap(old,
                                      new,
                                      Ordering::SeqCst) == old
        {
            Some(RwLockReadGuard {
                lock: &self.lock,
                data: unsafe { & *self.data.get() },
            })
        } else {
            None
        }
    }

The problem is that a different thread could increment the readers count on the indicated line, making the value that the CAS checks for incorrect - it might fail even if there are no writers at all. For example:

    let lock = Arc::new(spin::RwLock::new(0));

    {
        let lock = lock.clone();
        thread::spawn(move || {
            loop {
                lock.try_read().unwrap();
            }
        });
    }

    thread::spawn(move || {
        loop {
            // Increment and decrement readers count in a loop
            lock.read();
        }
    }).join();

This code panics for me, even though it there are no writers so try_read should always succeed.

To be frank, I think it would be better to simply rewrite this whole implementation. The original blog that this was implemented from uses the writers bit to allow for recursive write locking (which this crate doesn't) - I see no reason not to just use some sentinel value like -1 to indicate a writer is holding the lock.

@64 64 changed the title Buggy Drop impl for RwLockWriteGuard and issues in RwLock::try_read Wrong atomic ordering Drop impl for RwLockWriteGuard and incorrect lock acquisition failure in RwLock::try_read Jun 27, 2019
@64 64 changed the title Wrong atomic ordering Drop impl for RwLockWriteGuard and incorrect lock acquisition failure in RwLock::try_read Wrong atomic ordering in Drop impl for RwLockWriteGuard and buggy lock acquisition failure in RwLock::try_read Jun 27, 2019
@Ericson2314
Copy link
Contributor

Your first bug makes sense; blame that was my error years ago. Thanks for catching. I am confused by your second bug; if readers is incremented than the compare won't match and just another loop will happen: seems sub-optimal but not unsound?

Feel free to take a stab at a rewrite.

@64
Copy link
Contributor Author

64 commented Jun 28, 2019

The problem is that there is an implicit expectation that try_read will succeed if there are no writers. Or at least, you need to make it abundantly clear in the docs that this is not the case - i.e that you can't rely on try_read() succeeding even if there are no writers.

Working on a fix - would you be okay with an implementation which includes upgradeable locks? I'll try and do a benchmark and show that performance doesn't suffer as a result.

@64
Copy link
Contributor Author

64 commented Jun 28, 2019

forced_write_unlock also incorrectly uses Ordering::Relaxed.

@Ericson2314
Copy link
Contributor

Oh right! There is no loop. Yeah I think I just took the code from the original to make the try version, not realizing about that difference in expectations.

Upgradable is fine. I think std might even do that now?

jsirois added a commit to jsirois/pants that referenced this issue Sep 23, 2019
The vulnerability:
```
$ ./build-support/bin/ci.py --cargo-audit
...
error: Vulnerable crates found!

ID:	 RUSTSEC-2019-0013
Crate:	 spin
Version: 0.5.1
Date:	 2019-08-27
URL:	 mvdnes/spin-rs#65
Title:	 Wrong memory orderings in RwLock potentially violates mutual exclusion
Solution: upgrade to: >= 0.5.2

error: 1 vulnerability found!
Cargo audit failure
```

Although we don't directly depend on `spin`, we depend on `lazy_static`
(amongst others) which does:
```
$ (cd src/rust/engine && ../../../build-support/bin/native/cargo tree -p spin -i)
spin v0.5.2
├── lazy_static v1.3.0
...
```

So this change was generated with a targeted upgrade:
```
$ ./build-support/bin/native/cargo update --manifest-path src/rust/engine/Cargo.toml -p spin --aggressive
```
jsirois added a commit to pantsbuild/pants that referenced this issue Sep 24, 2019
The vulnerability:
```
$ ./build-support/bin/ci.py --cargo-audit
...
error: Vulnerable crates found!

ID:	 RUSTSEC-2019-0013
Crate:	 spin
Version: 0.5.1
Date:	 2019-08-27
URL:	 mvdnes/spin-rs#65
Title:	 Wrong memory orderings in RwLock potentially violates mutual exclusion
Solution: upgrade to: >= 0.5.2

error: 1 vulnerability found!
Cargo audit failure
```

Although we don't directly depend on `spin`, we depend on `lazy_static`
(amongst others) which does:
```
$ (cd src/rust/engine && ../../../build-support/bin/native/cargo tree -p spin -i)
spin v0.5.2
├── lazy_static v1.3.0
...
```

So this change was generated with a targeted upgrade:
```
$ ./build-support/bin/native/cargo update --manifest-path src/rust/engine/Cargo.toml -p spin --aggressive
```
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

Successfully merging a pull request may close this issue.

2 participants