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

VecDeque's Drain::drop writes to memory that a shared reference points to #60076

Closed
Tracked by #2528
RalfJung opened this issue Apr 18, 2019 · 21 comments · Fixed by #101299
Closed
Tracked by #2528

VecDeque's Drain::drop writes to memory that a shared reference points to #60076

RalfJung opened this issue Apr 18, 2019 · 21 comments · Fixed by #101299
Assignees
Labels
A-collections Area: `std::collection` C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@RalfJung
Copy link
Member

RalfJung commented Apr 18, 2019

liballoc's test_drain fails when run in Miri. The error occurs in the drain(...).collect() call:

  • collect is called with a vec_deque::Drain<usize> as argument. Drain contains Iter contains a shared reference to a slice; that slice is thus marked as "must not be mutated for the entire duration of this function call".
  • collect calls from_iter calls extend calls for_each calls fold, which eventually drops the Drain.
  • Drain::drop calls source_deque.wrap_copy to re-arrange stuff (I have not fully understood this yet), and in some cases this will end up writing to memory that the slice in Iter points to.

I am not sure what the best way to fix this is. We have to fix Drain holding (indirectly) a shared reference to something that it'll mutate during its Drop. The mutation is likely there for a reason, so I guess the shared reference has to go. (FWIW, this shared reference already caused other trouble, but that was fixed by #56161.)

Cc @nikomatsakis @gankro

@jonas-schievink jonas-schievink added A-collections Area: `std::collection` C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Apr 18, 2019
@Gankra
Copy link
Contributor

Gankra commented Apr 18, 2019

yeah I guess just change it to *const [T] and toss in a PhantomData for the borrow. shrug

@RalfJung
Copy link
Member Author

You mean, change this in Iter as well? Sure, that should work. In principle this reduces the precision of compiler analyses on VecDeque::iter().

@Gankra
Copy link
Contributor

Gankra commented Apr 18, 2019

ahh misread. maybe would just not use Iter then. depends on what the impl looks like now

@cuviper
Copy link
Member

cuviper commented Apr 18, 2019

  • Drain::drop calls source_deque.wrap_copy to re-arrange stuff (I have not fully understood this yet), and in some cases this will end up writing to memory that the slice in Iter points to.

But the first thing it does is self.for_each(drop); which should exhaust the Iter such that its ring is an empty slice. I suppose its pointer will still be somewhere in the deque's memory, but with length 0 it can't actually read anything. Is that not good enough to appease Miri?

@RalfJung
Copy link
Member Author

RalfJung commented Apr 19, 2019

@cuviper

But the first thing it does is self.for_each(drop); which should exhaust the Iter such that its ring is an empty slice. I suppose its pointer will still be somewhere in the deque's memory, but with length 0 it can't actually read anything. Is that not good enough to appease Miri?

No. This code still matches the pattern

fn foo(mut x: (T, &[U])) {
  let ptr = &x.1[0] as *const U;
  // do some stuff with x that "semantically" "exhausts" the slice.
  x.1 = &[];
  // then write to where `x.1` used to point to.
  *(ptr as *mut U) = ...;
}

There is no notion of "exhausting" or "consuming" a shared reference. When you create a shared reference with a certain lifetime, you promise that until that lifetime ends, no mutation happens. x here is mutable so you can change what the slice points to, but that has no effect at all on the guarantees that have been made about the value that was initially passed to foo.

Now, lifetimes are an entirely static concept, so for Stacked Borrows a "lifetime" generally is "until the pointer is used for the last time". However, for the special case of a reference being passed to a function, Stacked Borrows asserts that the "lifetime" will last at least as long as the function the reference is pointed to. (This is enforced by the "barriers" explained in this blog post.) This is extremely useful for optimizations.

@RalfJung
Copy link
Member Author

ahh misread. maybe would just not use Iter then. depends on what the impl looks like now

Drain::{next, next_back, size_hint} forward to Iter. I wouldn't want to copy those.

@cuviper
Copy link
Member

cuviper commented Apr 19, 2019

Oh, I also didn't realize that Iter doesn't actually shrink down to an empty slice -- it only updates its indexes. It would otherwise have to use two slices for the wrapped head and tail. So that full shared reference does still exist after the iterator is exhausted. (This also raises the validity question of having references to uninitialized data.)

Could we base Drain on IterMut instead? We could arrange wrap_copy to work through that mutable reference too, so there's no question of mutable aliasing.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 19, 2019

Could we base Drain on IterMut instead? We could arrange wrap_copy to work through that mutable reference too, so there's no question of mutable aliasing.

That might work. However, Drain::drop must not access deque.buf in that case -- that would be in conflict with the unique reference in IterMut.

@cuviper
Copy link
Member

cuviper commented Apr 19, 2019

Hmm, does Drain need to be covariant over T? I guess IterMut would break that.

@RalfJung
Copy link
Member Author

Actually this idea of "consuming a shared ref" is far from unique to VecDeque... the following code also errors in Miri:

use std::cell::{RefCell, Ref};

fn break_it(rc: &RefCell<i32>, r: Ref<'_, i32>) {
    // `r` has a shared reference, it is passed in as argument and hence
    // a barrier is added that marks this memory as read-only for the entire
    // duration of this function.
    drop(r);
    // *oops* here we can mutate that memory.
    *rc.borrow_mut() = 2;
}

fn main() {
    let rc = RefCell::new(0);
    break_it(&rc, rc.borrow())
}

@Julian-Wollersberger
Copy link
Contributor

As I understand it, Miri and stacked borrows should be "more powerful" than the Rust borrow checker, right?
So, is this an example where Miri does not match what Rust allows or is this a bug in Rust that could lead to undefined behaviour in safe code?
(Your example in the playground)

@cuviper
Copy link
Member

cuviper commented Apr 20, 2019

@Julius-Beides -- I believe @RalfJung is admitting that RefCell example as something that should be allowed. But this issue with VecDeque involves raw pointers, which rustc doesn't check at all, so miri is in uncharted waters here.

@RalfJung
Copy link
Member Author

RalfJung commented Apr 20, 2019

So, is this an example where Miri does not match what Rust allows or is this a bug in Rust that could lead to undefined behaviour in safe code?

This is up for us all to decide. I can't tell you. ;)

I believe @RalfJung is admitting that RefCell example as something that should be allowed.

Not necessary. I have argued that Ref/RefMut are problematic long before Stacked Borrows. The lifetimes there are, strictly speaking, wrong. I think it would be reasonable to rule that they have to use raw pointers.

@RalfJung
Copy link
Member Author

RalfJung commented Jun 9, 2019

FWIW here is a stand-alone version of the testcase to reproduce this with Miri (but it needs access to some private VecDeque internals):

use std::collections::VecDeque;

fn main() {
    let mut tester: VecDeque<usize> = VecDeque::with_capacity(3);

    let cap = tester.capacity();
    for len in 0..=cap {
        for tail in 0..=cap {
            for drain_start in 0..=len {
                for drain_end in drain_start..=len {
                    tester.tail = tail;
                    tester.head = tail;
                    for i in 0..len {
                        tester.push_back(i);
                    }

                    // Check that we drain the correct values
                    println!("Testing {} {} {} {}", len, tail, drain_start, drain_end);
                    let drained: VecDeque<_> = tester.drain(drain_start..drain_end).collect();
                    let drained_expected: VecDeque<_> = (drain_start..drain_end).collect();
                    assert_eq!(drained, drained_expected);

                    // We shouldn't have changed the capacity or made the
                    // head or tail out of bounds
                    assert_eq!(tester.capacity(), cap);
                    assert!(tester.tail < tester.cap());
                    assert!(tester.head < tester.cap());

                    // We should see the correct values in the VecDeque
                    let expected: VecDeque<_> = (0..drain_start)
                        .chain(drain_end..len)
                        .collect();
                    assert_eq!(expected, tester);
                }
            }
        }
    }
}

@RalfJung
Copy link
Member Author

RalfJung commented Aug 4, 2019

See rust-lang/unsafe-code-guidelines#125 for the issue about whether Stacked Borrows should allow such code or not.

For now, with rust-lang/miri#872, Miri accepts this code again. So I am going to close the issue here. It still represents an interesting data point to inform what Stacked Borrows rules should be.

@RalfJung RalfJung closed this as completed Aug 4, 2019
@Gankra
Copy link
Contributor

Gankra commented Aug 7, 2019

nods wisely

just gotta stand still long enough for the spec to bend around you

@RalfJung
Copy link
Member Author

RalfJung commented Aug 7, 2019

@gankro ;-)

I don't think the decision is made either way, but for now with several counterexamples in libstd, it is not helpful for Miri to complain about this. Plus, this matches what we formalized in Coq; the basic optimizations that we considered so far are all fine with this change.

@RalfJung
Copy link
Member Author

See #63787: there actually is a bug here right now, because we do emit noalias for Drain passed to a function. So what Miri found is actually UB in the LLVM IR that we are generating right now.

Reopening for now, but maybe it makes more sense to treat this as a duplicate of #63787.

@RalfJung RalfJung reopened this Aug 21, 2019
@Elinvynia Elinvynia added the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Jun 9, 2020
@LeSeulArtichaut LeSeulArtichaut removed the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Jun 9, 2020
@5225225
Copy link
Contributor

5225225 commented Aug 13, 2022

Smaller reproduction that I found in #99701 (a now closed dupe of this issue) that doesn't rely on VecDeque internals, that fails now with -Zmiri-retag-fields

use std::collections::VecDeque;

fn main() {
    let mut tester: VecDeque<usize> = VecDeque::with_capacity(3);

    for i in 0..3 {
        tester.push_back(i);
    }

    let _: VecDeque<_> = tester.drain(1..2).collect();
}

@RalfJung
Copy link
Member Author

RalfJung commented Aug 13, 2022

-Zmiri-retag-fields re-enables the part of Stacked Borrows that got disabled (in parts) to work around the issue, before I knew that we actually have noalias here and are risking real miscompilations.

bors added a commit to rust-lang/miri that referenced this issue Sep 1, 2022
Add a protector test that demonstrates the base tag diagnostic

Per #2519 (comment), this demonstrates this case for protector diagnostics:
```
help: <3131> was created here, as a base tag for alloc1623
  --> tests/fail/stacked_borrows/invalidate_against_protector3.rs:10:19
   |
10 |         let ptr = std::alloc::alloc(std::alloc::Layout::for_value(&0i32)) as *mut i32;
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```
This diagnostic is inspired by what Miri used to do with rust-lang/rust#60076 (comment)
@saethlin
Copy link
Member

saethlin commented Sep 2, 2022

@rustbot claim

@bors bors closed this as completed in 59e7a30 Sep 11, 2022
workingjubilee pushed a commit to tcdi/postgrestd that referenced this issue Sep 15, 2022
Remove &[T] from vec_deque::Drain

Fixes rust-lang/rust#60076

I don't know what the right approach is here. There were a few suggestions in the issue, and they all seem a bit thorny to implement. So I just picked one that was kind of familiar.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-collections Area: `std::collection` C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants