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

Confusing stacked borrows violation #1364

Closed
Robbepop opened this issue Apr 24, 2020 · 13 comments
Closed

Confusing stacked borrows violation #1364

Robbepop opened this issue Apr 24, 2020 · 13 comments
Labels
A-aliasing Area: This affects the aliasing model (Stacked/Tree Borrows) C-question Category: Not necessarily a bug, but someone asking for support

Comments

@Robbepop
Copy link
Contributor

Robbepop commented Apr 24, 2020

Running miri on one of our codebases revealed some violation of its stacked borrow model. https://github.com/pepyakin was able to craft a minimized version of the problem:

use std::cell::UnsafeCell;

#[test]
fn evil() {
    let array: [u8; 2] = [b'A', b'B'];
    let array = UnsafeCell::new(array);

    let (_loaded_a, _loaded_b) = unsafe {
        let a = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[0]
        };
        let b = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[1]
        };
        (a, b)
    };
}

#[test]
fn saint() {
    let array: [u8; 2] = [b'A', b'B'];
    let array = UnsafeCell::new(array);

    unsafe {
        let _loaded_a = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[0]
        };
        let _loaded_b = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[1]
        };
    }
}

While miri has no problems with the saint version it finds a stacked borrows violation in evil. Note that a difference between the two is that evil packs loaded_a and loaded_b into a tuple.

The output of miri of testing evil is the following:

running 1 test
note: tracking was triggered
   --> core/src/storage2/lazy/lazy_array.rs:462:31
    |
462 |                 NonNull::from(&mut cached_entries[1])
    |                               ^^^^^^^^^^^^^^^^^^^^^^ popped tracked tag for item [Unique for <1020215>]
    |
    = note: inside `storage2::lazy::lazy_array::tests::standalone_fails` at core/src/storage2/lazy/lazy_array.rs:462:31
note: inside closure at core/src/storage2/lazy/lazy_array.rs:452:5
   --> core/src/storage2/lazy/lazy_array.rs:452:5
    |
452 | /     fn standalone_fails() {
453 | |         let array = UnsafeCell::new([b'A', b'B']);
454 | |         let (_loaded_a, _loaded_b) = unsafe {
455 | |             let a = &mut *{
...   |
466 | |         };
467 | |     }
    | |_____^
    = note: inside `<[closure@core/src/storage2/lazy/lazy_array.rs:452:5: 467:6] as std::ops::FnOnce<()>>::call_once - shim` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `<fn() as std::ops::FnOnce<()>>::call_once - shim(fn())` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `test::__rust_begin_short_backtrace::<fn()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:517:5
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:508:30
    = note: inside `<[closure@DefId(38:631 ~ test[2197]::run_test[0]::{{closure}}[2]) 0:fn()] as std::ops::FnOnce<()>>::call_once - shim(vtable)` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send> as std::ops::FnOnce<()>>::call_once` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/boxed.rs:1008:9
    = note: inside `<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>> as std::ops::FnOnce<()>>::call_once` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:318:9
    = note: inside `std::panicking::r#try::do_call::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:331:40
    = note: inside `std::panicking::r#try::<(), std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:274:15
    = note: inside `std::panic::catch_unwind::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:394:14
    = note: inside `test::run_test_in_process` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:541:18
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:450:39
    = note: inside `test::run_test::run_test_inner` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:475:13
    = note: inside `test::run_test` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:505:28
    = note: inside `test::run_tests::<[closure@DefId(38:230 ~ test[2197]::console[0]::run_tests_console[0]::{{closure}}[2]) 0:&mut test::console::ConsoleTestState, 1:&mut std::boxed::Box<dyn test::formatters::OutputFormatter>]>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:284:13
    = note: inside `test::run_tests_console` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/console.rs:280:5
    = note: inside `test::test_main` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:120:15
    = note: inside `test::test_main_static` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:139:5
    = note: inside `main`
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:67:34
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:73
    = note: inside `std::sys_common::backtrace::__rust_begin_short_backtrace::<[closure@DefId(1:6032 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/sys_common/backtrace.rs:130:5
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:13
    = note: inside `std::panicking::r#try::do_call::<[closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:331:40
    = note: inside `std::panicking::r#try::<i32, [closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:274:15
    = note: inside `std::panic::catch_unwind::<[closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:394:14
    = note: inside `std::rt::lang_start_internal` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:51:25
    = note: inside `std::rt::lang_start::<()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:67:5
    = note: this note originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: Undefined Behavior: trying to reborrow for Unique, but parent tag <1020215> does not have an appropriate item in the borrow stack
   --> core/src/storage2/lazy/lazy_array.rs:465:14
    |
465 |             (a, b)
    |              ^ trying to reborrow for Unique, but parent tag <1020215> does not have an appropriate item in the borrow stack
    |
    = help: this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental
    = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
            
    = note: inside `storage2::lazy::lazy_array::tests::standalone_fails` at core/src/storage2/lazy/lazy_array.rs:465:14
note: inside closure at core/src/storage2/lazy/lazy_array.rs:452:5
   --> core/src/storage2/lazy/lazy_array.rs:452:5
    |
452 | /     fn standalone_fails() {
453 | |         let array = UnsafeCell::new([b'A', b'B']);
454 | |         let (_loaded_a, _loaded_b) = unsafe {
455 | |             let a = &mut *{
...   |
466 | |         };
467 | |     }
    | |_____^
    = note: inside `<[closure@core/src/storage2/lazy/lazy_array.rs:452:5: 467:6] as std::ops::FnOnce<()>>::call_once - shim` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `<fn() as std::ops::FnOnce<()>>::call_once - shim(fn())` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `test::__rust_begin_short_backtrace::<fn()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:517:5
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:508:30
    = note: inside `<[closure@DefId(38:631 ~ test[2197]::run_test[0]::{{closure}}[2]) 0:fn()] as std::ops::FnOnce<()>>::call_once - shim(vtable)` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232:5
    = note: inside `<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send> as std::ops::FnOnce<()>>::call_once` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/boxed.rs:1008:9
    = note: inside `<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>> as std::ops::FnOnce<()>>::call_once` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:318:9
    = note: inside `std::panicking::r#try::do_call::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:331:40
    = note: inside `std::panicking::r#try::<(), std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:274:15
    = note: inside `std::panic::catch_unwind::<std::panic::AssertUnwindSafe<std::boxed::Box<dyn std::ops::FnOnce() + std::marker::Send>>, ()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:394:14
    = note: inside `test::run_test_in_process` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:541:18
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:450:39
    = note: inside `test::run_test::run_test_inner` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:475:13
    = note: inside `test::run_test` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:505:28
    = note: inside `test::run_tests::<[closure@DefId(38:230 ~ test[2197]::console[0]::run_tests_console[0]::{{closure}}[2]) 0:&mut test::console::ConsoleTestState, 1:&mut std::boxed::Box<dyn test::formatters::OutputFormatter>]>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:284:13
    = note: inside `test::run_tests_console` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/console.rs:280:5
    = note: inside `test::test_main` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:120:15
    = note: inside `test::test_main_static` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libtest/lib.rs:139:5
    = note: inside `main`
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:67:34
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:73
    = note: inside `std::sys_common::backtrace::__rust_begin_short_backtrace::<[closure@DefId(1:6032 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/sys_common/backtrace.rs:130:5
    = note: inside closure at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:13
    = note: inside `std::panicking::r#try::do_call::<[closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:331:40
    = note: inside `std::panicking::r#try::<i32, [closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:274:15
    = note: inside `std::panic::catch_unwind::<[closure@DefId(1:6031 ~ std[4b50]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:394:14
    = note: inside `std::rt::lang_start_internal` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:51:25
    = note: inside `std::rt::lang_start::<()>` at /home/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:67:5
    = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error; 6 warnings emitted

test storage2::lazy::lazy_array::tests::evil ...
@Robbepop
Copy link
Contributor Author

Robbepop commented Apr 24, 2020

Pepyakin and me have further minimized both tests and found out the following:

#[test]
fn evil2() {
    let array: [u8; 2] = [b'A', b'B'];
    let array = UnsafeCell::new(array);

    let (_loaded_a, _loaded_b) = unsafe {
        let a = &mut (*array.get())[0];
        let b = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[1]
        };
        (a, b)
    };
}

#[test]
fn saint2() {
    let array: [u8; 2] = [b'A', b'B'];
    let array = UnsafeCell::new(array);

    let (_loaded_a, _loaded_b) = unsafe {
        let b = &mut *{
            let cached_entries = &mut *array.get();
            &mut cached_entries[1]
        };
        let a = &mut (*array.get())[0];
        (a, b)
    };
}

The problem is that there is a &mut reference to one of the array elements (e.g. array[0] and there is an access to the cached_entries that it a &mut reference to the array as a whole. The saint2 example demonstrates this by avoiding this scenario with just swapping let a and let b definitions compared to evil2.

So with the above minification we believe that miri might have actually found undefined behaviour and thus earned yet another trophy. This code would have definitely made it into production without us making use of miri to eventually be scared by its findings.

edit:

When trying to fix the real codebase using the above implications I tried to delay the pointer dereferencing a bit. Basically changing

unsafe { (
    &mut *self.load_through_cache(a).as_ptr(),
    &mut *self.load_through_cache(b).as_ptr(),
) };

to

unsafe {
    let loaded_a = self.load_through_cache(a).as_ptr();
    let loaded_b = self.load_through_cache(b).as_ptr();
    (&mut *loaded_a, &mut *loaded_b)
};

Which also fixed the problem for miri. I am officially confused now because this to my understanding contradicts the implications of above. I hope I am wrong.

@RalfJung
Copy link
Member

RalfJung commented Apr 25, 2020

What is confusing me here is why you are using UnsafeCell. UnsafeCell is crucial for interior mutability (i.e., mutability behind a shared reference &), but as long as you use &mut (and not &), UnsafeCell has no effect at all.

In this line:

let cached_entries = &mut *array.get();

you are creating a new mutable reference covering the entire array. Mutable references must be unique, so this pretty much says "there cannot be any other pointer to this array, this is the only one". Any other pointer that already exists becomes invalid, in particular a.

So, your original evil here definitely violates Rust's aliasing rules.

The reason saint works fine is that you are not using _loaded_a again. In saint, just like in evil, the a reference becomes invalid the moment you create the other mutable reference that overlaps it. But in saint you do not use that invalid mutable reference, so there is no error.

Btw, I am not sure if that's also useful for your original code, but you can write all this without any unsafe and this avoid all the aliasing trouble:

pub fn evil_fixed() {
    let mut array: [u8; 2] = [b'A', b'B'];
    
    let (a, rest) = array.split_first_mut().unwrap();
    let (b, _) = rest.split_first_mut().unwrap();
    
    // Now `a` and `b` are mutable references to the two array elements.
}

(I did not read your 2nd post yet, this is just responding to the original one.)

@Robbepop
Copy link
Contributor Author

Robbepop commented Apr 25, 2020

Just had a chat about this problem with @oli-obk and the conclusion of it was that

unsafe { (
    &mut *self.load_through_cache(a).as_ptr(),
    &mut *self.load_through_cache(b).as_ptr(),
) };

is failing miri's stacked borrows pass while

unsafe {
    let loaded_a = self.load_through_cache(a).as_ptr();
    let loaded_b = self.load_through_cache(b).as_ptr();
    (&mut *loaded_a, &mut *loaded_b)
};

it is the result of the first code example being translated by rustc to MIR in a way that it creates intermediate borrows of its inner elements within the tuple that extend the lifetimes provided by the second code example. Thus miri correctly inferes that borrow rules are invalidated in the first example, however, not so in the second. (I hope this was a correct summary of the problem.)

While this is a strange conclusion to me the problem is now fixed and we can actually close this. :)

@RalfJung
Copy link
Member

(Now responding to the 2nd post)

unsafe {
    let loaded_a = self.load_through_cache(a).as_ptr();
    let loaded_b = self.load_through_cache(b).as_ptr();
    (&mut *loaded_a, &mut *loaded_b)
};

I think this works "by accident" -- when you use raw pointers, Miri currently is not very strict at all, so that we can start by finding the much more pressing issues with people using references wrong. But longer-term we will need to change that. And if self.load_through_cache(b).as_ptr() creates a mutable reference that overlaps with loaded_a, then this code is definitely still wrong. (I don't know what load_through_cache does so I cannot tell.)

In that post you also have &mut (*array.get())[0], and that is indeed much better! Here, you are only creating a mutable reference to that one array element. So you only claim to have a unique pointer to that element, unlike your original code which claim to have a unique pointer to the entire array.

But if this fails

unsafe { (
    &mut *self.load_through_cache(a).as_ptr(),
    &mut *self.load_through_cache(b).as_ptr(),
) };

then there likely still is a bug, as the second call is invalidating the first reference.

@RalfJung RalfJung added A-aliasing Area: This affects the aliasing model (Stacked/Tree Borrows) C-question Category: Not necessarily a bug, but someone asking for support labels Apr 25, 2020
@Robbepop
Copy link
Contributor Author

(Now responding to the 2nd post)

unsafe {
    let loaded_a = self.load_through_cache(a).as_ptr();
    let loaded_b = self.load_through_cache(b).as_ptr();
    (&mut *loaded_a, &mut *loaded_b)
};

I think this works "by accident" -- when you use raw pointers, Miri currently is not very strict at all, so that we can start by finding the much more pressing issues with people using references wrong. But longer-term we will need to change that. And if self.load_through_cache(b).as_ptr() creates a mutable reference that overlaps with loaded_a, then this code is definitely still wrong. (I don't know what load_through_cache does so I cannot tell.)

In that post you also have &mut (*array.get())[0], and that is indeed much better! Here, you are only creating a mutable reference to that one array element. So you only claim to have a unique pointer to that element, unlike your original code which claim to have a unique pointer to the entire array.

But if this fails

unsafe { (
    &mut *self.load_through_cache(a).as_ptr(),
    &mut *self.load_through_cache(b).as_ptr(),
) };

then there likely still is a bug, as the second call is invalidating the first reference.

Please note that the first example are just minimizations of the second post examples.
So what load_through_cache does is basically return a mutable reference to the element within an array of the given index where the index is either a or b where a != b. That's why I think the code should be correct since we simply take an array and yield two &mut reference to two different elements of it back. The only difference in the 2nd example is that for a very short time we have a &mut reference to one of the elements of the array and the array itself for a short period of time whereas in the second example of the second post we avoid this by delaying turning the raw pointer into a reference which heavily confuses me. @oli-obk told me that miri does not really make a difference between raw pointers and references in accordance to the stacked borrows rules but you now said that there is a difference.

@Robbepop
Copy link
Contributor Author

What is confusing me here is why you are using UnsafeCell. UnsafeCell is crucial for interior mutability (i.e., mutability behind a shared reference &), but as long as you use &mut (and not &), UnsafeCell has no effect at all.

In this line:

let cached_entries = &mut *array.get();

you are creating a new mutable reference covering the entire array. Mutable references must be unique, so this pretty much says "there cannot be any other pointer to this array, this is the only one". Any other pointer that already exists becomes invalid, in particular a.

So, your original evil here definitely violates Rust's aliasing rules.

The reason saint works fine is that you are not using _loaded_a again. In saint, just like in evil, the a reference becomes invalid the moment you create the other mutable reference that overlaps it. But in saint you do not use that invalid mutable reference, so there is no error.

Btw, I am not sure if that's also useful for your original code, but you can write all this without any unsafe and this avoid all the aliasing trouble:

pub fn evil_fixed() {
    let mut array: [u8; 2] = [b'A', b'B'];
    
    let (a, rest) = array.split_first_mut().unwrap();
    let (b, _) = rest.split_first_mut().unwrap();
    
    // Now `a` and `b` are mutable references to the two array elements.
}

(I did not read your 2nd post yet, this is just responding to the original one.)

Usage of UnsafeCell is important but not reflected in the minimized code example. I am sorry for this. If you want to view the original unminimized code please visit the following links:

So we need the UnsafeCell because we have to manipulate state of some entity in a &self method. Also we do not use safer variants of cells, e.g. RefCell because we need to yield references back to the callers. Basically what we do is very similar to being a cache from a database. Also these are the low-level abstractions of the library upon which other safe abstraction build on top to encapsulate unsafe code to these few low-level abstractions. So it is even more important that these low-level abstractions are correct and do not employ undefined behaviour.

@RalfJung
Copy link
Member

So what load_through_cache does is basically return a mutable reference to the element within an array of the given index where the index is either a or b where a != b. That's why I think the code should be correct since we simply take an array and yield two &mut reference to two different elements of it back.

That seems reasonable.
However, note that your code, in the process of doing that, creates a mutable reference to the entire array, and then uses indexing to narrow that down to the element at index a. That is not okay.

The only difference in the 2nd example is that for a very short time we have a &mut reference to one of the elements of the array and the array itself for a short period of time whereas in the second example of the second post we avoid this by delaying turning the raw pointer into a reference which heavily confuses me.

Exactly. And that is also a problem with the variant that delays creating the mutable reference: you have a raw pointer, and then you create a mutable reference (not derived from that raw pointer) that overlaps with the raw pointer. That makes the raw pointer invalid. Miri just does not catch that because you later create another raw pointer and it "confused" the two raw pointers because we do not track precisely enough.

@RalfJung
Copy link
Member

RalfJung commented Apr 25, 2020

This line here is a problem:
https://github.com/paritytech/ink/blob/redo-init-and-flush/core/src/storage2/lazy/lazy_array.rs#L320

&mut *self.cached_entries.get() creates a new unique reference to the entire array, which means "all other pointers previously derived from self.cached_entries to anywhere in this array are now invalid". You need to use raw pointers all the way, until you arrived at the specific element you are looking for, and only then create a reference. By using raw pointers you are acknowledging to Rust that the memory they point to is not exclusively yours, there could be others that claim ownership of this memory.

This looks like it does the same:
https://github.com/paritytech/ink/blob/redo-init-and-flush/core/src/storage2/lazy/lazy_imap.rs#L241
The reason Miri does not complain in the second example you were confused by is that Miri "confused the two raw pointers" as I mentioned above.

I thought by making Miri accept more code I'd help people, here it seems that got confusing. ;)

@Robbepop
Copy link
Contributor Author

Robbepop commented Apr 25, 2020

This line here is a problem:
https://github.com/paritytech/ink/blob/redo-init-and-flush/core/src/storage2/lazy/lazy_array.rs#L320

&mut *self.cached_entries.get() creates a new unique reference to the entire array, which means "all other pointers previously derived from self.cached_entries to anywhere in this array are now invalid". You need to use raw pointers all the way, until you arrived at the specific element you are looking for, and only then create a reference. By using raw pointers you are acknowledging to Rust that the memory they point to is not exclusively yours, there could be others that claim ownership of this memory.

This looks like it does the same:
https://github.com/paritytech/ink/blob/redo-init-and-flush/core/src/storage2/lazy/lazy_imap.rs#L241
The reason Miri does not complain in the second example you were confused by is that Miri "confused the two raw pointers" as I mentioned above.

Wow okay thank you for the explanation.
I am terryfied - seems like miri earned yet another trophy (or two).
I have a solution for the array case - simply wrap all elements in UnsafeCell instead of the outer array. However, for the lazy_imap that is using a BTreeMap as cache it is a bit more involved since we actually need to mutate the caching BTreeMap. So your suggestion there is to simply return pointers instead of references in load_through_cache since it changes semantics in a way that does not make invokations like in swap employ UB. Is that correct?

Seems like I am super confused by now. The load_through_cache already returns a NonNull<Entry<T>> which is basically a non-null raw pointer from my understanding. So we should already be fine here if we simply delay pointer-to-reference conversion. I am probably wrong though.

I thought by making Miri accept more code I'd help people, here it seems that got confusing. ;)

I actually consider using miri in our CI for exactly the reason that it does not accept broken code - that's actually a feature. ;)

@Robbepop
Copy link
Contributor Author

Robbepop commented Apr 25, 2020

This looks like it does the same:
https://github.com/paritytech/ink/blob/redo-init-and-flush/core/src/storage2/lazy/lazy_imap.rs#L241
The reason Miri does not complain in the second example you were confused by is that Miri "confused the two raw pointers" as I mentioned above.

There actually is another difference because the imap stores its entries with an additional indirection in Box to avoid pointer invalidation upon mutating the outer BTreeMap. The thing is that the swap function in the imap example doesn't employ the delayed reference-to-pointer trick to confuse miri and still works.

@RalfJung
Copy link
Member

I have a solution for the array case - simply wrap all elements in UnsafeCell instead of the outer array.

If you then use shared references to the array of UnsafeCell (again, acknowledging to Rust that there could be mutating aliases), then that should work, yes.

However, for the lazy_imap that is using a BTreeMap as cache it is a bit more involved since we actually need to mutate the caching BTreeMap. So your suggestion there is to simply return pointers instead of references in load_through_cache since it changes semantics in a way that does not make invokations like in swap employ UB. Is that correct?

swap never creating mutable references is definitely a start, yes -- that seems just unnecessary.
But you will have to be careful when loading the second pointer, to not invalidate the first one.

But since you said later you have an indirection with a Box there, that should actually be fine! Looking up the second pointer in the BTree will not touch the data inside the box in any way.

So this is probably actually okay then:
https://github.com/paritytech/ink/blob/e434da5ab6ddc22d6cbd6d89a19675cd75b403e5/core/src/storage2/lazy/lazy_imap.rs#L241
Here you are saying that you have unique access to the BTree, and that's actually correct; the reference you already created in swap doesnt point inside the BTree, it points to the box.

That is probably why you are not seeing Miri failures with the BTree thing.

@Robbepop
Copy link
Contributor Author

Okay thank you again for the explanation. I now think I got a grasp of the underlying problem. Thank you a lot!
I will open another PR for the miri trophy you earned. Afterall without miri pointing this subtle error out this code would have probably landed in master or even production. 🙃

Robbepop added a commit to Robbepop/miri that referenced this issue Apr 25, 2020
Details to the found in rust-lang#1364.
Note that this was not a found in a `master` or production release of ink!, however without analysing the code via `miri` this could have potentially happened.
@RalfJung
Copy link
Member

All right, I am closing this ticket then. Glad that it was helpful :)

bors added a commit that referenced this issue Apr 25, 2020
…alfJung

Add miri trophy for LazyArray::swap (ink! PR)

Details to the found in #1364.
Note that this was not a found in a `master` or production release of ink!, however without analysing the code via `miri` this could have potentially happened.
bors added a commit that referenced this issue Apr 25, 2020
…alfJung

Add miri trophy for LazyArray::swap (ink! PR)

Details to the found in #1364.
Note that this was not a found in a `master` or production release of ink!, however without analysing the code via `miri` this could have potentially happened.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-aliasing Area: This affects the aliasing model (Stacked/Tree Borrows) C-question Category: Not necessarily a bug, but someone asking for support
Projects
None yet
Development

No branches or pull requests

2 participants