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

TB: Reserved + Protected + IM + lazy is a horrible combination that should not exist #3742

Merged
merged 6 commits into from
Jul 16, 2024

Conversation

Vanille-N
Copy link
Contributor

As discovered by @JoJoDeveloping, the result of having both Protector exceptions on lazy locations (protectors only protect initialized bytes) and interior mutability exceptions for protected tags (Reserved IM does not accept foreign writes when protected) leads to some very undesirable results, namely that we cannot do spurious writes even on protected activated locations.

We propose that Protected Reserved IM should no longer exist and instead when a type is retagged as part of a FnEntry it is assumed to lose interior mutability.

In fact, this was already being done implicitly because relevant transitions were guarded by an if protected, but the difference is that now it also applies to transitions that occur after the end of the protector.

src/borrow_tracker/tree_borrows/mod.rs Outdated Show resolved Hide resolved
tests/fail/tree_borrows/reservedim_spurious_write.rs Outdated Show resolved Hide resolved

fn example(spurious: bool) {
// For this example it is important that we have at least two bytes
// because lazyness is involved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does that need two bytes? You can have 1 byte and an &() reference, and then laziness is still visible, no?

Copy link
Contributor

@JoJoDeveloping JoJoDeveloping Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But () is not interior mutable. In general, using ZSTs here suggests the counterexample relies on "wonky" ZST semantics, even worse if we cook up an Interior mutable ZST type (UnsafeCell<()>), because what does that even mean? It would make the thing actually happening here much more obscure. And also, the counterexample is independent of whatever happens at ZST retags. It still occurs when the language has no notion of zero-sized objects at all.

Copy link
Member

@RalfJung RalfJung Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes an UnsafeCell<()> accessed outside its range more wonky than an UnsafeCell<u8> outside its range?

The counterexample is independent of what happens in one of the two bytes that you have here, so having both bytes is somewhat confusing. It's not about zero-sized objects, it's about a reference that only has "lazy" accesses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if you have an array of UnsafeCell<u8> and go to the next element you naturally do an actual memory access, but if you iterate an array of UnsafeCell<()> you're actually doing no memory accesses at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think I just understood the point. We wouldn't have two elements, we'd just have one single Cell<u8> that we retag as &mut *(r as *Cell<u8> as *Cell<()>) as *mut Cell<()> as *mut Cell<u8> I guess ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoJoDeveloping not sure why array iteration would suddenly be relevant?

@Vanille-N yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that @JoJoDeveloping interpreted your comment "you can have 1 byte and a &() reference" similarly to what I first did. Initially I thought you were arguing for using (Cell<()>, Cell<u8>) rather than [Cell<u8>; 2] but actually it would just be a single Cell<u8>.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I disagree with it being a single byte allocation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand your reasoning for that. There's a single byte that's actually relevant for this, and all this mucking about with shifting the pointer back and forth is a distraction. The key point is that the relevant byte is not inside the range that is initialized upon reborrow.

tests/fail/tree_borrows/reservedim_spurious_write.rs Outdated Show resolved Hide resolved
- split test into two revisions
- clarify comments
@Vanille-N
Copy link
Contributor Author

Btw the exhaustive tests need to be updated, but it's a bit more work because now not all configurations are valid as initial permissions. There's nothing incorrect right now because we're just testing more configurations.
When we do update the tests we'll be able to simplify the state machine so that foreign_write no longer depends on protected.

@RalfJung
Copy link
Member

When we do update the tests we'll be able to simplify the state machine so that foreign_write no longer depends on protected.

foreign_read still depends on it though I assume? But magically that actually does not cause problems?

@Vanille-N
Copy link
Contributor Author

foreign_read still depends on it though I assume? But magically that actually does not cause problems?

foreign_read needs protected to know if Active becomes Frozen or Disabled.
foreign_write still implicitly depends on protected, but because !ty_is_freeze implies !protected the dependency kind of disappears in the implementation of the transition itself.

@JoJoDeveloping
Copy link
Contributor

JoJoDeveloping commented Jul 10, 2024

magically that actually does not cause problems

It does indeed not caue problems. The only other place it's relevant is on Active nodes, and there the protectors ending causes a write event (introduced in #3732). And nothing (protected) survives foreign writes.

@RalfJung
Copy link
Member

Test LGTM now, thanks!

Btw the exhaustive tests need to be updated, but it's a bit more work because now not all configurations are valid as initial permissions. There's nothing incorrect right now because we're just testing more configurations. When we do update the tests we'll be able to simplify the state machine so that foreign_write no longer depends on protected.

What is the plan with this? Should it happen in this PR or a future PR?

@Vanille-N
Copy link
Contributor Author

Worth noting that now conflicted and ty_is_freeze are not orthogonal: maybe instead of (bool, bool) we should have a 3-variant enum (Activable | Conflicted | InteriorMut).
That and optimizing the exhaustive tests could be done simultaneously, probably in a different PR.

@RalfJung
Copy link
Member

It seems fine to not do that in this PR, but then please leave a FIXME at the relevant place(s) in the code.

@RalfJung
Copy link
Member

LGTM, thanks!
@bors r+

@bors
Copy link
Contributor

bors commented Jul 16, 2024

📌 Commit c0c1028 has been approved by RalfJung

It is now in the queue for this repository.

@bors
Copy link
Contributor

bors commented Jul 16, 2024

⌛ Testing commit c0c1028 with merge b11f82b...

@bors
Copy link
Contributor

bors commented Jul 16, 2024

☀️ Test successful - checks-actions
Approved by: RalfJung
Pushing b11f82b to master...

@bors bors merged commit b11f82b into rust-lang:master Jul 16, 2024
8 checks passed
bors added a commit that referenced this pull request Aug 16, 2024
Make unused states of Reserved unrepresentable

In the [previous TB update](#3742) we discovered that the existence of `Reserved + !ty_is_freeze + protected` is undesirable.

This has the side effect of making `Reserved { conflicted: true, ty_is_freeze: false }` unreachable.
As such it is desirable that this state would also be unrepresentable.

This PR eliminates the unused configuration by changing
```rs
enum PermissionPriv {
    Reserved { ty_is_freeze: bool, conflicted: bool },
    ...
}
```
into
```rs
enum PermissionPriv {
    ReservedFrz { conflicted: bool },
    ReservedIM,
    ...
}
```
but this is not the only solution and `Reserved(Activable | Conflicted | InteriorMut)` could be discussed.
In addition to making the unreachable state not representable anymore, this change has the nice side effect of enabling `foreign_read` to no longer depend explicitly on the `protected` flag.

Currently waiting for
- `@JoJoDeveloping` to confirm that this is the same representation of `Reserved` as what is being implemented in simuliris,
- `@RalfJung` to approve that this does not introduce too much overhead in the trusted codebase.
RalfJung pushed a commit to RalfJung/rust that referenced this pull request Aug 18, 2024
Make unused states of Reserved unrepresentable

In the [previous TB update](rust-lang/miri#3742) we discovered that the existence of `Reserved + !ty_is_freeze + protected` is undesirable.

This has the side effect of making `Reserved { conflicted: true, ty_is_freeze: false }` unreachable.
As such it is desirable that this state would also be unrepresentable.

This PR eliminates the unused configuration by changing
```rs
enum PermissionPriv {
    Reserved { ty_is_freeze: bool, conflicted: bool },
    ...
}
```
into
```rs
enum PermissionPriv {
    ReservedFrz { conflicted: bool },
    ReservedIM,
    ...
}
```
but this is not the only solution and `Reserved(Activable | Conflicted | InteriorMut)` could be discussed.
In addition to making the unreachable state not representable anymore, this change has the nice side effect of enabling `foreign_read` to no longer depend explicitly on the `protected` flag.

Currently waiting for
- `@JoJoDeveloping` to confirm that this is the same representation of `Reserved` as what is being implemented in simuliris,
- `@RalfJung` to approve that this does not introduce too much overhead in the trusted codebase.
lnicola pushed a commit to lnicola/rust-analyzer that referenced this pull request Sep 25, 2024
Make unused states of Reserved unrepresentable

In the [previous TB update](rust-lang/miri#3742) we discovered that the existence of `Reserved + !ty_is_freeze + protected` is undesirable.

This has the side effect of making `Reserved { conflicted: true, ty_is_freeze: false }` unreachable.
As such it is desirable that this state would also be unrepresentable.

This PR eliminates the unused configuration by changing
```rs
enum PermissionPriv {
    Reserved { ty_is_freeze: bool, conflicted: bool },
    ...
}
```
into
```rs
enum PermissionPriv {
    ReservedFrz { conflicted: bool },
    ReservedIM,
    ...
}
```
but this is not the only solution and `Reserved(Activable | Conflicted | InteriorMut)` could be discussed.
In addition to making the unreachable state not representable anymore, this change has the nice side effect of enabling `foreign_read` to no longer depend explicitly on the `protected` flag.

Currently waiting for
- `@JoJoDeveloping` to confirm that this is the same representation of `Reserved` as what is being implemented in simuliris,
- `@RalfJung` to approve that this does not introduce too much overhead in the trusted codebase.
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 this pull request may close these issues.

4 participants