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

Stacked Borrows does not support &UnsafeCell pointing to read-only data #303

Open
oconnor663 opened this issue Aug 24, 2021 · 10 comments
Open
Labels
A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows)

Comments

@oconnor663
Copy link

I'm playing with the idea of a ReadOnlyCell<T> type, like a Cell<T> that only supports reading. The (kind of academic) goal of this type is to allow conversions from either &T or &Cell<T> into &ReadOnlyCell<T>, similar to how Cell::from_mut converts from &mut T to &Cell<T>. Here's the example code (playground link):

use std::cell::{Cell, UnsafeCell};

#[repr(transparent)]
pub struct ReadOnlyCell<T>(UnsafeCell<T>);

impl<T> ReadOnlyCell<T> {
    pub fn new(val: T) -> Self {
        Self(UnsafeCell::new(val))
    }

    pub fn from_ref(t: &T) -> &Self {
        // SAFETY: &ReadOnlyCell<T> is strictly less capable than &T
        unsafe { &*(t as *const T as *const Self) }
    }

    pub fn from_cell_ref(t: &Cell<T>) -> &Self {
        // SAFETY: &ReadOnlyCell<T> is strictly less capable than &Cell<T>
        unsafe { &*(t as *const Cell<T> as *const Self) }
    }
}

impl<T: Copy> ReadOnlyCell<T> {
    pub fn get(&self) -> T {
        unsafe { *self.0.get() }
    }
}

fn main() {
    let my_int = &42;
    let read_only_cell = ReadOnlyCell::from_ref(my_int);
    assert_eq!(read_only_cell.get(), 42);
}

This fails Miri:

error: Undefined Behavior: trying to reborrow for SharedReadWrite at alloc45, but parent tag <untagged> does not have an appropriate item in the borrow stack
  --> src/main.rs:13:18
   |
13 |         unsafe { &*(t as *const T as *const Self) }
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ trying to reborrow for SharedReadWrite at alloc45, but parent tag <untagged> 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 `ReadOnlyCell::<i32>::from_ref` at src/main.rs:13:18
note: inside `main` at src/main.rs:30:26
  --> src/main.rs:30:26
   |
30 |     let read_only_cell = ReadOnlyCell::from_ref(my_int);
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It seems like Miri is telling me that converting &T to &UnsafeCell<T> is inherently UB. That's surprising to me. Obviously writing to that UnsafeCell would be UB. But here all I think I'm doing is telling the compiler not to treat the pointer as noalias, which is sort of like converting &T to *const T, which is legal. Am I thinking about this the right way?

@RalfJung RalfJung transferred this issue from rust-lang/miri Aug 30, 2021
@RalfJung
Copy link
Member

Hm, yeah that seems like a fair point. Current Stacked Borrows cannot really handle this case -- it considers just the type of the pointer (roughly &UnsafeCell in this case) to compute the required permission, but cannot take into account the previous permission of the pointer.

This is a fundamental problem with Stacked Borrows, not its implementation in Miri, so I moved the issue. Thanks for bringing this up!

@RalfJung RalfJung changed the title miri failure for a ReadOnlyCell reference conversion that should(?) be legal Stacked Borrows does not support &UnsafeCell pointing to read-only data Aug 30, 2021
@RalfJung RalfJung added the A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows) label Aug 30, 2021
@RalfJung
Copy link
Member

This is related to #227 and possibly #133.

@hudson-ayers
Copy link

As mentioned in #314 , we have also run into this issue in Tock. Our original solution was basically casting &[T] to &[UnsafeCell<T>], but this causes miri to complain.

One suggested alternative is to transmute the initialized memory we recieve to MaybeUninit<T> instead, since MaybeUninit<T> does not allow the compiler to assume multiple reads of the underlying type will be fixed, and there does not seem to be a requirement (like for UnsafeCell) that the memory being placed inside a MaybeUninit point to writeable data. Miri no longer complains with this change made.

However, this does seem like at-best an abuse of MaybeUninit outside of its intended purpose. Is using MaybeUninit as a sort of ReadOnlyUnsafeCell just hiding our unsoundness from Miri, or does this make sense as a potential solution to the problem described in this issue?

@comex
Copy link

comex commented Jan 19, 2022

One suggested alternative is to transmute the initialized memory we recieve to MaybeUninit<T> instead, since MaybeUninit<T> does not allow the compiler to assume multiple reads of the underlying type will be fixed,

This characterization in terms of not being "fixed" is not accurate. Your link mentioned this comes from the documentation for MaybeUninit, but that documentation is also wrong.

Read this: "What The Hardware Does" is not What Your Program Does: Uninitialized Memory

Strictly speaking, it's not that the bytes can change values when you read them multiple times. On the contrary, from the abstract machine's perspective, reading a byte of uninitialized memory always gives you the same value. It's just that that byte value is not a number from 0 to 255, but instead a special value called "uninitialized". (From LLVM's perspective, this is represented by either undef or poison.) The blog post explains this in more detail.

The only effect of MaybeUninit is that it's not instant UB for it to contain uninitialized bytes. For most types, you get UB as soon as you create a value of that type containing uninitialized bytes, where by "create" I mean, e.g., performing a load of uninitialized memory with that type, or calling assume_init() on a MaybeUninit that isn't actually initialized, etc. In this case, though, your data doesn't(?) actually contain uninitialized bytes, so using MaybeUninit or not shouldn't make any difference.

However, I don't know why Miri treats it differently.

@jettr
Copy link

jettr commented Jan 19, 2022

Thank you for the guidance; it is really helpful!

Our miri issue that we are trying to fix is related to transmuting &[u8] into &[Cell<u8>] and miri tells us that we have introduce the possibility of interior mutability where there should be none. The fix is to instead transmute &[u8] into &[MaybeUninit<u8>], which does seem to correctly fix the issue miri is reporting (not just making it difficult for miri to detect).

The question now though is if we are breaking aliasing rules by using MaybeUninit instead of Cell. We also can create a &[MaybeUninit<u8>] or &[Cell<u8>] from a raw pointer of initialized memory that is somewhat outside of our control. The complication comes from the fact that the data in the underlying buffer can be changed externally while we have a immutable reference to it. Currently we are relying on Cell to provide a new read every time we access an element in the buffer, and can we instead rely on MaybeUninit::as_ptr().read() to provide the same level of guarantees?

The data that MaybeUninit holds is never actually uninitialized in our case.

@RalfJung
Copy link
Member

RalfJung commented Jan 19, 2022

The question now though is if we are breaking aliasing rules by using MaybeUninit instead of Cell

Yes, you are. &MaybeUninit is read-only like all shared references without an UnsafeCell.

I don't know why Miri no longer complains after that change; without a small example there are just too many variables here. But adding a MaybeUninit does not help at all when it comes to shared mutation.

@Lokathor
Copy link
Contributor

Cell does not ensure that each read is a fresh read. For that you'd need some sort of volatile based abstraction.

@RalfJung
Copy link
Member

RalfJung commented Aug 5, 2022

When fixing this we have to be careful not to accept this program -- it segfaults at runtime, so we better make sure it does have UB.

@RalfJung
Copy link
Member

Tree Borrows fixes this issue.

@oconnor663
Copy link
Author

Very exciting!

oconnor663 added a commit to oconnor663/pyo3 that referenced this issue Nov 16, 2023
Is there any practical use for these conversions? I'm not sure :) But
they're spiritually similar to the Cell::from_mut conversion in the
standard library. This makes it possible to write a function taking
`&ReadOnlyCell<T>` if you only need to copy the `T` and you don't need
the guarantee that it won't change from one read to the next (which it
could, if other `&Cell<T>` references exist pointing to the same value).

With these conversions in place, the relationships between the different
reference types look like this:

```
                     &mut T
                    /      \
                coerce   Cell::from_mut
                  /          \
                 &T       &Cell<T>
                  \          /
ReadOnlyCell::from_ref    ReadOnlyCell::from_cell
                    \      /
                 &ReadOnlyCell<T>
```

This sort of thing wasn't supported by Miri until recently, but the new
Tree Borrows model supports it:
rust-lang/unsafe-code-guidelines#303.

The new test currently fails Miri by default:

```
$ cargo +nightly miri test test_read_only_cell_conversions
...
error: Undefined Behavior: trying to retag from <747397> for SharedReadWrite permission at
alloc222352[0x0], but that tag only grants SharedReadOnly permission for this location
```

But it passes with `-Zmiri-tree-borrows`:

```
$ MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test test_read_only_cell_conversions
...
test buffer::tests::test_read_only_cell_conversions ... ok
```
oconnor663 added a commit to oconnor663/pyo3 that referenced this issue Nov 16, 2023
Is there any practical use for these conversions? I'm not sure :) But
they're spiritually similar to the `Cell::from_mut` conversion in the
standard library. This makes it possible to write a function taking
`&ReadOnlyCell<T>` if you only need to copy the `T` and you don't need
the guarantee that it won't change from one read to the next (which it
could, if other `&Cell<T>` references exist pointing to the same value).

With these conversions in place, the relationships between the different
reference types look like this:

```
                     &mut T
                    /      \
                coerce   Cell::from_mut
                  /          \
                 &T       &Cell<T>
                  \          /
ReadOnlyCell::from_ref    ReadOnlyCell::from_cell
                    \      /
                 &ReadOnlyCell<T>
```

This sort of thing wasn't supported by Miri until recently, but the new
Tree Borrows model supports it:
rust-lang/unsafe-code-guidelines#303.

The new test currently fails Miri by default:

```
$ cargo +nightly miri test test_read_only_cell_conversions
...
error: Undefined Behavior: trying to retag from <747397> for SharedReadWrite permission at
alloc222352[0x0], but that tag only grants SharedReadOnly permission for this location
```

But it passes with `-Zmiri-tree-borrows`:

```
$ MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test test_read_only_cell_conversions
...
test buffer::tests::test_read_only_cell_conversions ... ok
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-aliasing-model Topic: Related to the aliasing model (e.g. Stacked/Tree Borrows)
Projects
None yet
Development

No branches or pull requests

6 participants