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

Loom does not consider dropping UnsafeCell to be a write #349

Open
e00E opened this issue Apr 23, 2024 · 6 comments · May be fixed by #351
Open

Loom does not consider dropping UnsafeCell to be a write #349

e00E opened this issue Apr 23, 2024 · 6 comments · May be fixed by #351

Comments

@e00E
Copy link

e00E commented Apr 23, 2024

Consider the following code:

#[test]
fn foo() {
    loom::model(|| {
        let a: loom::cell::UnsafeCell<()> = Default::default();
        let first_ref = a.get();
        let second_ref = a.get_mut();
    });
}

The test fails because loom correctly detects that mutable access is not allowed while there is already immutable access.

Now change the last line of the code:

#[test]
fn foo() {
    loom::model(|| {
        let a: loom::cell::UnsafeCell<()> = Default::default();
        let first_ref = a.get();
        std::mem::drop(a);
    });
}

Loom no longer detects an error.

This is surprising because dropping could be considered a write. For example, if instead of () I was using a type with a custom Drop implementation, then I would get a mutable reference to self. This write doesn't go through UnsafeCell but it still leads to the reference guarantees being violated.

This behavior might be intentional because the documentation for ConstPtr says that Loom doesn't track liveness. I'm not sure. I did not find an existing issue about this topic so I created one. If the behavior is intentional, then this issue can be closed and can serve as documentation for the reason.

@hawkw
Copy link
Member

hawkw commented Apr 23, 2024

This behavior might be intentional because the documentation for ConstPtr says that Loom doesn't track liveness. I'm not sure. I did not find an existing issue about this topic so I created one. If the behavior is intentional, then this issue can be closed and can serve as documentation for the reason.

Loom doesn't track liveness of pointers in the general case, because that would be very challenging to implement in loom (and is arguably better served by other tools, such as miri), but, IMO, that doesn't mean we shouldn't handle this specific case. We probably should treat dropping an UnsafeCell as a mutable access, IMO.

e00E added a commit to e00E/tokio that referenced this issue Apr 23, 2024
The code that we're removing calls UnsafeCell::with_mut with the
argument `std::mem::drop`. This is misleading because the use of `drop`
has no effect. `with_mut` takes an argument of type
`impl FnOnce(*mut T) -> R`. The argument to the argument function is a
pointer. Dropping a pointer has no effect.

The comment above the first instance of this pattern claims that this
releases some resource. This is false because the call has no effect.
The intention might have been to drop the value behind the pointer. If
this did happen, it would be a bug because the resource (`waker`) would
be dropped again at the end of the function when the containing object
is dropped.

I looked through the history of this code. This code originally called
`with_mut` with the argument `|_| ()`. Calling `with_mut` with an
argument function that does nothing has a side effect when testing with
loom. When testing with loom, the code uses loom's UnsafeCell type
instead of std's. The intention of the code was likely to make use of
that side effect because we expect to have exclusive access here as we
are going to drop the containing object. The side effect is that loom
checks that Rust's reference uniqueness properties are upheld.

Instead of removing the whole code, I considered changing `drop` back to
`|_|()` and documenting what I wrote above. I decided on removing the
code because nowhere else do we use `with_mut` in this way and the
purpose of this check would be better served in loom directly as part of
UnsafeCell's Drop implementation. I created an issue about this in loom
[1].

[1] tokio-rs/loom#349
@e00E
Copy link
Author

e00E commented Apr 24, 2024

I've thought about the implementation of this. The naive (is there another?) way to implement this is to implement Drop and get a mutable reference while dropping. That way you detect other active references. This is awkward for two reasons:

  • You restrict the type's use relative to std's UnsafeCell. When Drop is implemented, drop check applies, making some uses that worked before no longer work.
  • into_inner stops working because you can't destructure Self anymore. We can fix that by storing an Option.

@Darksonn
Copy link
Contributor

Let's try with a Drop implementation for now. As for into_inner, we can implement it unsafely.

e00E added a commit to e00E/tokio that referenced this issue Apr 24, 2024
The code that we're removing calls UnsafeCell::with_mut with the
argument `std::mem::drop`. This is misleading because the use of `drop`
has no effect. `with_mut` takes an argument of type
`impl FnOnce(*mut T) -> R`. The argument to the argument function is a
pointer. Dropping a pointer has no effect.

The comment above the first instance of this pattern claims that this
releases some resource. This is false because the call has no effect.
The intention might have been to drop the value behind the pointer. If
this did happen, it would be a bug because the resource (`waker`) would
be dropped again at the end of the function when the containing object
is dropped.

I looked through the history of this code. This code originally called
`with_mut` with the argument `|_| ()`. Calling `with_mut` with an
argument function that does nothing has a side effect when testing with
loom. When testing with loom, the code uses loom's UnsafeCell type
instead of std's. The intention of the code was likely to make use of
that side effect because we expect to have exclusive access here as we
are going to drop the containing object. The side effect is that loom
checks that Rust's reference uniqueness properties are upheld.

To continue to check this, I have only removed the use of `drop` while
keeping `with_mut`. It would be even better to have loom check this
implicitly when UnsafeCell is dropped. I created an issue about this in
loom [1].

[1] tokio-rs/loom#349
e00E added a commit to e00E/loom that referenced this issue Apr 24, 2024
Dropping an UnsafeCell or calling into_inner can be seen as mutable
access, which is not allowed to happen while another borrow exists.

The downside of this change is the added Drop implementation of
UnsafeCell. This restricts some uses of UnsafeCell that were previously
allowed related to drop check.

fixes tokio-rs#349
e00E added a commit to e00E/loom that referenced this issue Apr 24, 2024
Dropping an UnsafeCell or calling into_inner can be seen as mutable
access, which is not allowed to happen while another borrow exists.

The downside of this change is the added Drop implementation of
UnsafeCell. This restricts some uses of UnsafeCell that were previously
allowed related to drop check.

I removed one of the tests because it stops working with this change
due to double panic. Fixing it is awkward.

I went with an approach using unsafe. It is possible to implement this
safely but it has too much overhead. We would have to wrap the data
field with Option to allow safely taking. This is fine but we would also
need to Box it because the type parameter T is ?Sized, which cannot be
put into Option.

fixes tokio-rs#349
@e00E e00E linked a pull request Apr 24, 2024 that will close this issue
Darksonn pushed a commit to tokio-rs/tokio that referenced this issue Apr 25, 2024
The code that we're removing calls UnsafeCell::with_mut with the
argument `std::mem::drop`. This is misleading because the use of `drop`
has no effect. `with_mut` takes an argument of type
`impl FnOnce(*mut T) -> R`. The argument to the argument function is a
pointer. Dropping a pointer has no effect.

The comment above the first instance of this pattern claims that this
releases some resource. This is false because the call has no effect.
The intention might have been to drop the value behind the pointer. If
this did happen, it would be a bug because the resource (`waker`) would
be dropped again at the end of the function when the containing object
is dropped.

I looked through the history of this code. This code originally called
`with_mut` with the argument `|_| ()`. Calling `with_mut` with an
argument function that does nothing has a side effect when testing with
loom. When testing with loom, the code uses loom's UnsafeCell type
instead of std's. The intention of the code was likely to make use of
that side effect because we expect to have exclusive access here as we
are going to drop the containing object. The side effect is that loom
checks that Rust's reference uniqueness properties are upheld.

To continue to check this, I have only removed the use of `drop` while
keeping `with_mut`. It would be even better to have loom check this
implicitly when UnsafeCell is dropped. I created an issue about this in
loom [1].

Links: tokio-rs/loom#349 [1]
e00E added a commit to e00E/loom that referenced this issue Apr 25, 2024
Dropping an UnsafeCell or calling into_inner can be seen as mutable
access, which is not allowed to happen while another borrow exists.

The downside of this change is the added Drop implementation of
UnsafeCell. This restricts some uses of UnsafeCell that were previously
allowed related to drop check.

I went with an approach using unsafe. It is possible to implement this
safely but it has too much overhead. We would have to wrap the data
field with Option to allow safely taking. This is fine but we would also
need to Box it because the type parameter T is ?Sized, which cannot be
put into Option.

fixes tokio-rs#349
@wyfo
Copy link

wyfo commented Jul 14, 2024

Actually, I think both the examples written in the issue shouldn't fail, because there is no error according to borrowing/aliasing rules.
Using miri, this code runs fine:

let cell = std::cell::UnsafeCell::new(0);
let shared = unsafe { &*cell.get() };
let exclusive = unsafe { &mut *cell.get() };
drop(cell);

What is not fine is to access shared after exclusive declaration for example.

It makes me think that loom behavior is too much conservative here, because it is tracking the lifetime of the pointers (which shouldn't have lifetime in fact), instead of tracking their accesses as miri does.
A solution could be to change ConstPtr::deref/MutPtr::deref to return a guard instead, and only raise an error if the guard is alive when there is a concurrent exclusive access, or if the cell has been dropped.

@wyfo
Copy link

wyfo commented Jul 14, 2024

Moreover, an interesting thing I've noticed with miri: the following example doesn't fail

let cell = std::cell::UnsafeCell::new(0);
let shared = unsafe { &*cell.get() };
drop(cell);
dbg!(*shared);

Because UnsafeCell<i32> doesn't need to be dropped, it seems that miri doesn't consider drop(cell) as an invalidation. I've asked about it in zulip.
But if this behavior is correct, we would need to take std::mem::needs_drop<T>() result in account to invalidate the cell when it's dropped. Actually, because we cannot track the dropping of a potential parent struct as miri does, it is not possible to take std::mem::needs_drop in account.

@e00E
Copy link
Author

e00E commented Jul 15, 2024

It is a good observation that access is the true problem. I touch on this in the OP and hawkw mentions it too. Search for "liveness". As hawkw writes, it is hard for loom to be as correct as miri for this case. The loom check is an approximation of the correct check.

My examples technically do not access the cell contents. I omitted this for brevity because it doesn't matter since loom does not enforce this. Imagine that the examples had extra lines like this:

unsafe { first_ref.deref() };
unsafe { second_ref.deref() };

This is what loom is meant to help catch.

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.

4 participants