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

Can mutate in match-arm using a closure #27282

Closed
arielb1 opened this issue Jul 25, 2015 · 23 comments · Fixed by #63059
Closed

Can mutate in match-arm using a closure #27282

arielb1 opened this issue Jul 25, 2015 · 23 comments · Fixed by #63059
Labels
A-NLL Area: Non-lexical lifetimes (NLL) A-typesystem Area: The type system C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness NLL-fixed-by-NLL Bugs fixed, but only when NLL is enabled. P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@arielb1
Copy link
Contributor

arielb1 commented Jul 25, 2015

Update from pnkfelix: Note that this is now fixed by NLL (see #50783). This bug just remains open because @pnkfelix thinks our policy is to keep NLL-fixed-by-NLL bugs open until we make NLL the default for rustc.


STR

fn main() {
    match Some(&4) {
        None => {},
        ref mut foo
            if {
                (|| { let bar = foo; bar.take() })();
                false
            } => {},
        Some(s) => println!("{}", *s)
    }
}

Actual Results

playpen: application terminated abnormally with signal 4 (Illegal instruction)
@eefriedman
Copy link
Contributor

Ideally, the type of "foo" in the pattern guard should be &Option<&i32> rather than &mut Option<&i32>... then we could loosen the overly restrictive mutation rules in pattern guards.

We might be able to specifically check for this case at

fn check_for_mutation_in_guard<'a, 'tcx>(cx: &'a MatchCheckCtxt<'a, 'tcx>,
; the problem there is that it could end up spuriously detecting a mutable borrow.

Another possibility would be to make it impossible to move out of variables of type &mut _; this is probably something we want to do anyway to eliminate the distinction between let bar = foo; (which moves) and let foo : &mut _ = foo (which reborrows).

@steveklabnik steveklabnik added the A-typesystem Area: The type system label Aug 13, 2015
@steveklabnik steveklabnik added A-compiler I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Feb 7, 2017
@steveklabnik
Copy link
Member

This compiles with no error today. Nominating.

@arielb1
Copy link
Contributor Author

arielb1 commented Feb 7, 2017

Fix by MIR borrowck. Forgot to tag as I-unsound myself.

@pnkfelix
Copy link
Member

Oddly this seems to be on the road to be fixed (as in flagged as error) by -Z borrowck=mir, but then when you add -Z nll to the mix, the error is again missed.

@nikomatsakis
Copy link
Contributor

The NLL analysis not wrong exactly, but there is a kind of implicit borrow that's not manifest in the MIR. In particular, the NLL analysis determines that the ref mut foo is only used in one particular basic block, and hence there are no overlapping uses of the value being matched.

That is, the MIR is sort of doing piecemeal tests that read the value of the option, but assuming that the option will not change in the meantime. We can guarantee that it won't by taking a borrow, but we don't do that. So effectively we get this:

let mut v = Some(4);

if v.is_none() { ... }

let mut p = &mut v;
(|| { p.take(); () })(); // clears `v`

let s = v.unwrap(); // panics here, but UB in MIR

except that the final unwrap() would be UB.

At least that's how I read it now.

@pnkfelix pnkfelix added A-NLL Area: Non-lexical lifetimes (NLL) WG-compiler-nll labels Dec 21, 2017
@pnkfelix
Copy link
Member

for now I'm labelled this with the NLL labels. We may not be able to include a fix for this in the initial release, but it would be nice to try...

@nikomatsakis nikomatsakis added this to the NLL Future Compat Warnings milestone Jan 4, 2018
@nikomatsakis
Copy link
Contributor

Seems like we have to fix this. I'm not sure just what the best way is -- but I think we have to represent the "implicit borrow" in the MIR.

@pnkfelix pnkfelix self-assigned this Jan 10, 2018
@pnkfelix pnkfelix added P-high High priority and removed P-medium Medium priority labels Jan 10, 2018
@pnkfelix
Copy link
Member

Assigning to self and up'ing priority to P-high to reflect severity for deploying NLL.

@pnkfelix
Copy link
Member

I am currently exploring extending MIR with a way to express "borrow all the discriminants reachable from place" so that we can freeze the variant alone over the course of all patterns+guards for a given match. Hopefully that can address this bug.

@nikomatsakis
Copy link
Contributor

@pnkfelix I've been having second thoughts on that approach. For example, consider this:

let mut b = true;
match b {
    true => ...
    false => ...
}

no "discriminant" is touched, but we want to know that no writes occur.

I think we should think again about idea of shared borrow of the entire value being matched and then having guards access their bindings through a &T (where T is the user-visible type of the binding). To handle ref mut bindings, we can consider creating (for the guard) a ref binding and using a cast to coerce the &'a &'a T to a &'a &'a mut T (which I believe are identical in power anyhow).

That last part is a hack, no question, but overall the approach is pretty elegant, and seems like it would cover all cases.

Thoughts?

@pnkfelix
Copy link
Member

Seems like my suggestion (from our private conversation) of an explicit EndBorrow would also handle the case you mention

But I’m willing to try your suggestion. Either direction has some hackery to it

@pnkfelix
Copy link
Member

pnkfelix commented Jul 24, 2018

(but I am going to remove it from the "NLL invalid code does not compile" milestone because it is fixed by NLL... Update: ah niko is too quick for me.)

pnkfelix added a commit to pnkfelix/rust that referenced this issue Jul 26, 2018
Also convert an ICE that became reachable code under borrowck=migrate
into a normally reported error (which is then downgraded to a
warning). This actually has a nice side benefit of providing a
somewhat more useful error message, at least in the particular case of
the example from issue rust-lang#27282.
bors added a commit that referenced this issue Sep 24, 2018
[NLL] Be more permissive when checking access due to Match

Partially addresses #53114. notably, we should now have parity with AST borrowck. Matching on uninitialized values is still forbidden.

* ~~Give fake borrows for match their own `BorrowKind`~~
* ~~Allow borrows with this kind to happen on values that are already mutably borrowed.~~
* ~~Track borrows with this type even behind shared reference dereferences and consider all accesses to be deep when checking for conflicts with this borrow type. See [src/test/ui/issues/issue-27282-mutate-before-diverging-arm-3.rs](cb5c989#diff-a2126cd3263a1f5342e2ecd5e699fbc6) for an example soundness issue this fixes (a case of #27282 that wasn't handled correctly).~~
* Create a new `BorrowKind`: `Shallow` (name can be bike-shed)
* `Shallow` borrows differ from shared borrows in that
  * When we check for access we treat them as a `Shallow(Some(_))` read
  * When we check for conflicts with them, if the borrow place is a strict prefix of the access place then we don't consider that a conflict.
    * For example, a `Shallow` borrow of `x` does not conflict with any access or borrow of `x.0` or `*x`
* Remove the current fake borrow in matches.
* When building matches, we take a `Shallow` borrow of any `Place` that we switch on or bind in a match, and any prefix of those places. (There are some optimizations where we do fewer borrows, but this shouldn't change semantics)
  * `match x { &Some(1) => (),  _ => (), }` would `Shallow` borrow `x`, `*x` and `(*x as Some).0` (the `*x` borrow is unnecessary, but I'm not sure how easy it would be to remove.)
* Replace the fake discriminant read with a `ReadForMatch`.
* Change ReadForMatch to only check for initializedness (to prevent `let x: !; match x {}`), but not conflicting borrows. It is still considered a use for liveness and `unsafe` checking.
* Give special cased error messages for this kind of borrow.

Table from the above issue after this PR

| Thing | AST | MIR | Want | Example |
| --- | --- | --- | --- |---|
| `let _ = <unsafe-field>` | 💚  | 💚  | ❌ |  [playground](https://play.rust-lang.org/?gist=bb7843e42fa5318c1043d04bd72abfe4&version=nightly&mode=debug&edition=2015) |
| `match <unsafe_field> { _ => () }` | ❌  | ❌ | ❌ | [playground](https://play.rust-lang.org/?gist=3e3af05fbf1fae28fab2aaf9412fb2ea&version=nightly&mode=debug&edition=2015) |
| `let _ = <moved>` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=91a6efde8288558e584aaeee0a50558b&version=nightly&mode=debug&edition=2015) |
| `match <moved> { _ => () }` | ❌ | ❌  | 💚 | [playground](https://play.rust-lang.org/?gist=804f8185040b2fe131f2c4a64b3048ca&version=nightly&mode=debug&edition=2015) |
| `let _ = <borrowed>` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=0e487c2893b89cb772ec2f2b7c5da876&version=nightly&mode=debug&edition=2015) |
| `match <borrowed> { _ => () }` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=0e487c2893b89cb772ec2f2b7c5da876&version=nightly&mode=debug&edition=2015) |

r? @nikomatsakis
bors added a commit that referenced this issue Sep 25, 2018
[NLL] Be more permissive when checking access due to Match

Partially addresses #53114. notably, we should now have parity with AST borrowck. Matching on uninitialized values is still forbidden.

* ~~Give fake borrows for match their own `BorrowKind`~~
* ~~Allow borrows with this kind to happen on values that are already mutably borrowed.~~
* ~~Track borrows with this type even behind shared reference dereferences and consider all accesses to be deep when checking for conflicts with this borrow type. See [src/test/ui/issues/issue-27282-mutate-before-diverging-arm-3.rs](cb5c989#diff-a2126cd3263a1f5342e2ecd5e699fbc6) for an example soundness issue this fixes (a case of #27282 that wasn't handled correctly).~~
* Create a new `BorrowKind`: `Shallow` (name can be bike-shed)
* `Shallow` borrows differ from shared borrows in that
  * When we check for access we treat them as a `Shallow(Some(_))` read
  * When we check for conflicts with them, if the borrow place is a strict prefix of the access place then we don't consider that a conflict.
    * For example, a `Shallow` borrow of `x` does not conflict with any access or borrow of `x.0` or `*x`
* Remove the current fake borrow in matches.
* When building matches, we take a `Shallow` borrow of any `Place` that we switch on or bind in a match, and any prefix of those places. (There are some optimizations where we do fewer borrows, but this shouldn't change semantics)
  * `match x { &Some(1) => (),  _ => (), }` would `Shallow` borrow `x`, `*x` and `(*x as Some).0` (the `*x` borrow is unnecessary, but I'm not sure how easy it would be to remove.)
* Replace the fake discriminant read with a `ReadForMatch`.
* Change ReadForMatch to only check for initializedness (to prevent `let x: !; match x {}`), but not conflicting borrows. It is still considered a use for liveness and `unsafe` checking.
* Give special cased error messages for this kind of borrow.

Table from the above issue after this PR

| Thing | AST | MIR | Want | Example |
| --- | --- | --- | --- |---|
| `let _ = <unsafe-field>` | 💚  | 💚  | ❌ |  [playground](https://play.rust-lang.org/?gist=bb7843e42fa5318c1043d04bd72abfe4&version=nightly&mode=debug&edition=2015) |
| `match <unsafe_field> { _ => () }` | ❌  | ❌ | ❌ | [playground](https://play.rust-lang.org/?gist=3e3af05fbf1fae28fab2aaf9412fb2ea&version=nightly&mode=debug&edition=2015) |
| `let _ = <moved>` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=91a6efde8288558e584aaeee0a50558b&version=nightly&mode=debug&edition=2015) |
| `match <moved> { _ => () }` | ❌ | ❌  | 💚 | [playground](https://play.rust-lang.org/?gist=804f8185040b2fe131f2c4a64b3048ca&version=nightly&mode=debug&edition=2015) |
| `let _ = <borrowed>` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=0e487c2893b89cb772ec2f2b7c5da876&version=nightly&mode=debug&edition=2015) |
| `match <borrowed> { _ => () }` | 💚  | 💚 | 💚 | [playground](https://play.rust-lang.org/?gist=0e487c2893b89cb772ec2f2b7c5da876&version=nightly&mode=debug&edition=2015) |

r? @nikomatsakis
@pnkfelix pnkfelix removed their assignment Nov 9, 2018
@pnkfelix
Copy link
Member

NLL (migrate mode) is enabled in all editions as of PR #59114.

The only policy question is that, under migrate mode, we only emit a warning on this (unsound) test case. Therefore, I am not 100% sure whether we should close this until that warning has been turned into a hard-error under our (still in development) future-compatibility lint policy.

Centril added a commit to Centril/rust that referenced this issue Jul 28, 2019
Centril added a commit to Centril/rust that referenced this issue Jul 29, 2019
…ewjasper

Make `#![feature(bind_by_move_pattern_guards)]` sound without `#[feature(nll)]`

Implements rust-lang#15287 (comment).

Fixes rust-lang#31287
Fixes rust-lang#27282

r? @matthewjasper
Centril added a commit to Centril/rust that referenced this issue Jul 30, 2019
…ewjasper

Make `#![feature(bind_by_move_pattern_guards)]` sound without `#[feature(nll)]`

Implements rust-lang#15287 (comment).

Fixes rust-lang#31287
Fixes rust-lang#27282

r? @matthewjasper
Centril added a commit to Centril/rust that referenced this issue Jul 30, 2019
Centril added a commit to Centril/rust that referenced this issue Aug 1, 2019
…ewjasper

Make `#![feature(bind_by_move_pattern_guards)]` sound without `#[feature(nll)]`

Implements rust-lang#15287 (comment) making `#![feature(bind_by_move_pattern_guards)]]` sound without also having `#![feature(nll)]`. The logic here is that if we see a `match` guard, we will refuse to downgrade NLL errors to warnings. This is in preparation for hopefully stabilizing the former feature in rust-lang#63118.

As fall out from the implementation we also:
Fixes rust-lang#31287
Fixes rust-lang#27282

r? @matthewjasper
bors added a commit that referenced this issue Aug 3, 2019
Make `#![feature(bind_by_move_pattern_guards)]` sound without `#[feature(nll)]`

Implements #15287 (comment) making `#![feature(bind_by_move_pattern_guards)]]` sound without also having `#![feature(nll)]`. The logic here is that if we see a `match` guard, we will refuse to downgrade NLL errors to warnings. This is in preparation for hopefully stabilizing the former feature in #63118.

As fall out from the implementation we also:
Fixes #31287
Fixes #27282

r? @matthewjasper
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 3, 2023
…wck-test, r=Nilstrieb

Test the borrowck behavior of if-let guards

Add some tests to make sure that if-let guards behave the same as if guards with respect to borrow-checking. Most of them are a naive adaptation, replacing an `if` guard with `if let Some(())`.
This includes regression tests for notable issues that arose for if guards (rust-lang#24535, rust-lang#27282, rust-lang#29723, rust-lang#31287) as suggested in rust-lang#51114 (comment).

cc `@pnkfelix` are there any other tests that you would want to see?
cc tracking issue rust-lang#51114
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-NLL Area: Non-lexical lifetimes (NLL) A-typesystem Area: The type system C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness NLL-fixed-by-NLL Bugs fixed, but only when NLL is enabled. P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants