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

BiLockGuard allows data race due to insufficient bound #2569

Closed
Qwaz opened this issue Feb 8, 2022 · 1 comment · Fixed by #2570
Closed

BiLockGuard allows data race due to insufficient bound #2569

Qwaz opened this issue Feb 8, 2022 · 1 comment · Fixed by #2570
Labels
A-lock Area: futures::lock bug

Comments

@Qwaz
Copy link

Qwaz commented Feb 8, 2022

Bug Description

unsafe impl<T: Send> Send for Inner<T> {}
unsafe impl<T: Send> Sync for Inner<T> {}

BiLockGuard implements Send for T: Send and Sync for T: Send that are propagated from manual unsafe Send/Sync impl for Inner. However, BiLockGuard implements Deref for T which requires T: Sync bound for Sync. This definition allows parallel access to T: !Sync type, which is unsound.

Observation on impact

BiLock API is unstable, so hopefully the bug wouldn't affect a lot of people. I also lightly audited io split and stream split, two places where BiLock is internally used. All of them seem to call as_pin_mut() right after locking which I believe to be sound with the current definition (but it's not a guarantee!)

Proof of Concept

Tested with futures 0.3.21.

Code:

#![forbid(unsafe_code)]

use std::cell::Cell;
use std::sync::Arc;
use std::thread;

use futures::lock::BiLock;

#[derive(Clone, Copy)]
enum RefOrInt {
    Ref(&'static u64),
    Int(u64),
}

static STATIC_INT: u64 = 123;

impl Default for RefOrInt {
    fn default() -> Self {
        RefOrInt::Ref(&STATIC_INT)
    }
}

#[tokio::main]
async fn main() {
    let (lock1, _lock2) = BiLock::new(Cell::new(RefOrInt::default()));

    // Contrived use case for easier demonstration
    let lock1 = Box::leak(Box::new(lock1));
    let guard1 = Arc::new(lock1.lock().await);
    let guard2 = guard1.clone();

    thread::spawn(move || {
        loop {
            // Repeatedly write Ref(&addr) and Int(0xdeadbeef) into the cell.
            guard1.set(RefOrInt::Ref(&STATIC_INT));
            guard1.set(RefOrInt::Int(0xdeadbeef));
        }
    });

    loop {
        if let RefOrInt::Ref(addr) = guard2.get() {
            if addr as *const u64 == &STATIC_INT as *const u64 {
                continue;
            }

            // We got Ref(0xdeadbeef) due to data race
            println!("Pointer is now: {:p}", addr);
            println!("Dereferencing addr will now segfault");
            println!("{}", addr);
        }
    }
}

Output:

Pointer is now: 0xdeadbeef
Dereferencing addr will now segfault
segmentation fault (core dumped)
@taiki-e taiki-e added bug A-lock Area: futures::lock labels Feb 9, 2022
@taiki-e
Copy link
Member

taiki-e commented Feb 13, 2022

@Qwaz Thanks for finding this! I filed #2570 to fix this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lock Area: futures::lock bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants