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

borrow-checker allows partial reinit of struct that has been moved away, but no use of it. #21232

Closed
pnkfelix opened this issue Jan 16, 2015 · 42 comments

Comments

@pnkfelix
Copy link
Member

commented Jan 16, 2015

Here is some sample code:

#[derive(Show)]
struct Pair { lft: u8, rgt: u8, }

fn main() {
    let mut p = Pair { lft: 1, rgt: 2 };
    let mut f = move |&mut:| { p.lft = 3; p.rgt };
    p.rgt = 4;
    println!("f: {:?}", f());
    p.lft = 5;
    // println!("p: {:?}", p);
}

This compiles without an error; running it prints f: 2u8.

If you uncomment the last line, you get an error saying "error: use of moved value: p". Likewise if you just attempt to print individual fields.

While I do see the logic being used here ("we are not overwriting the value within the closure itself; we are writing into the uninitialized memory that resulted from moving p into the closure"), it seems potentially confusing.

Is there any reason that we allowing these writes, which cannot be subsequently read? It seems like a situation analogous to moving into a moved-away array (see rust-lang/rfcs#533 )

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Jan 16, 2015

(however, unlike rust-lang/rfcs#533, it would not be all that terrible for non-zeroing dynamic-drop if we did not get around to making this illegal; we're already going to have to support representing structure fragments to implement non-zeroing dynamic drop, and initializations like the one in this ticket are just another case of that. Plus we might in the future add the ability to track such partial initializations and allow reads from initialized fields in partially-initialized structs.)

@mbrubeck

This comment has been minimized.

Copy link
Contributor

commented Mar 3, 2015

If this doesn't become an error, it should at least be a warning. Code like this is almost certainly a logic error of some kind, but currently compiles without error or warning:

struct Foo { x: i32 }
let mut f = Foo { x: 0 };
drop(f);
f.x = 1;
@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 3, 2015

triage: P-backcompat-lang, 1.0 beta

(but do see comments above noting that its not the end of the world if we did not fix this.)

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 3, 2015

triage: P-backcompat-lang (1.0 beta)

just trying to accommodate highfive...

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Mar 5, 2015

Interesting, I see the bug being that you get an error at all -- that is, I think that once you have fully "reinitialized" the structure, you should be able to read it again. (Rather than the ability to partially reinit the struct as being an error.)

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Mar 5, 2015

I'm going to tag this as I-Needs-Decision.

@mbrubeck

This comment has been minimized.

Copy link
Contributor

commented Mar 5, 2015

Note that this is currently an error if the moved value has a destructor. For example, if you add impl Drop for Foo {fn drop(&mut self) {}} to the previous example:

struct Foo { x: i32 }
impl Drop for Foo { fn drop(&mut self) {} }
let mut f = Foo { x: 0 };
drop(f);
f.x = 1;

then it is rejected with "error: partial reinitialization of uninitialized structure f."

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 5, 2015

at this point I think I would be in favor saying that we can live with the current semantics as is today, and fix things in the future backwards-compatibly to either 1. track the initialized parts and allow reading from them, as I mentioned at the end of my comment, or 2. the slightly more limited (but perhaps more in line with our current compiler infrastructure) approach of tracking when the entire (non-Drop) structure is reinitialized and then allow reads from it, as outlined in niko's comment.

So, bascially, I'm willing to reclassify this as a P-low, not 1.0 bug.

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 5, 2015

triage: P-low ()

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 5, 2015

(apparently empty brackets does not clears the milestone, though they may have at one point?)

@pnkfelix pnkfelix removed this from the 1.0 beta milestone Mar 5, 2015

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Mar 5, 2015

@mbrubeck also, if you wanted to try to develop a lint to warn about this case, I would not object (assuming it did not have too many false positives).

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Mar 10, 2015

@mbrubeck yes, we are more restrictive about structs with destructors, because in that case it doesn't make sense to drop one part and not another.

@logannc

This comment has been minimized.

Copy link

commented Jun 30, 2015

Real quick question after seeing this on Reddit:

When we are "writing into the uninitialized memory that resulted from moving p into the closure", where does that written value go? Does the compiler actually emit write instructions? If so, could that lead to a vulnerability by overwriting an important field?

(I would check the LLVM output myself, but I wouldn't have a clue how to read it.)

@huonw

This comment has been minimized.

Copy link
Member

commented Jun 30, 2015

It will generate writes to p's stack slot, but it shouldn't cause any problems: see also #26665.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Jun 30, 2015

@logannc If one omits the assignment to a:

fn main() {
    let mut a = A{val: 1};
    let a_ = a;
    /// a.val = 2;   // In the original example this line is uncommented
    println!("a_: {}", a_.val);
    /// println!("a: {}", a.val);  // error: use of moved value: `a.val` ?!?!
}

the optimizer should omit the stack slot of a since assigning to a and then moving into a_ should be optimized into just initializing a_. However, the code in the example is equivalent to just initializing a_ and having an uninitialized a variable that is initialized later:

fn main() {
    let mut a : A; // uninitialized
    let a_ = A{val: 1};
    a.val = 2;   // Initialize a here
    println!("a_: {}", a_.val);
    /// println!("a: {}", a.val);  // error: use of possibly uninitialized variable
}

However, I do not understand why using a reinitialized variable results in an error, e.g. by uncommenting the lines in the above examples.

It seems that writing to uninitialized memory:

  • does not reinitialize the variable, and thus
  • the value that was written cannot be used.

This makes no sense: either one cannot write to uninitialized memory, or doing so reinitializes the variable so that it can be used. Being able to write to uninitialized memory but not being able to use that value is weird.

@steveklabnik

This comment has been minimized.

Copy link
Member

commented Nov 4, 2015

Triage: updated code

#[derive(Debug)]
struct Pair { lft: u8, rgt: u8, }

fn main() {
    let mut p = Pair { lft: 1, rgt: 2 };
    let mut f = move || { p.lft = 3; p.rgt };
    p.rgt = 4;
    println!("f: {:?}", f());
    p.lft = 5;
    println!("p: {:?}", p);
}

Same error happens.

@pnkfelix pnkfelix added the T-lang label Jan 11, 2016

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 4, 2018

Discussed at lang team meeting.

For reference, here are the three example programs we discused (assuming we're starting with e.g. struct S { x: u32, y: u32 } and S does not implement Drop):

  • Accepted today: fn main() { let mut s: S; s.x = 10; }
  • Rejected today: fn main() { let mut s: S; s.x = 10; s.y = 30; a_use_of(s); }
  • Rejected today: fn main() { let mut s: S; s.x = 10; a_use_of(s.x); })

There was generally strong agreement that in the long term, it would be good to accept all three cases. I.e., we should allow building up a record in a piecemeal fashion, and it would also be good to allow reading a field after one has written to it, regardless of the original initialization state of the struct overall.

  • And it was also agreed that this is suitable as a long term goal. We don't need to try to do it e.g. for the edition.

There was some lukewarm support that, in the short term, we should be rejecting all three cases in the name of consistency.

  • This lukewarm support was based in part on the assumption that it would not be difficult to add to NLL
  • And it was also coupled with the shared understanding that it be solely added to NLL (where the 2018 edition's use of borrowck=migrate will imply that even if this does inject a borrow_check-error into someone's existing code, if that code previously compiled with the 2015 edition, then that borrow_check-error will be downgraded to a warning by the migrate machinery.)

@nikomatsakis hypothesized during the meeting that implementing this would be perhaps a day's worth of thought and/or programming effort.

With that in mind, we have tentatively decided to attempt, for the short-term, to adopt the semantics that rejects all three cases. (I.e., it will become an error to write to a field of a struct if that whole struct is not already initialized)

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 4, 2018

the effort here, i.e. the new check described in my previous comment, needs to happen soon if its going to happen at all.

So I'm retagging this as I-nominated to make it crystal clear that I now want this discussed at the NLL meeting, if we haven't already found a volunteer to implement this by that time.

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 8, 2018

Assigning to @spastorino; they are going to drive the initial effort on making it an error, for the short-term, to write to a field of a struct if that whole struct has not be already initialized.

Once that is done, we should either leave this issue open and assign it to someone working on the longer term project for supporting partial-assignments of uninitialized records, or close this issue and open a fresh issue to track the longer term project.

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 9, 2018

Putting on the RC2 milestone. If we're doing this change at all, its gotta land by then IMO.

@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 11, 2018

Its been getting hard in discussions to be clear about what "this fixes #21232" means when there are distinct short-term and long-term goals attached to that one issue number.

So I forked off #54986 and #54987 to cover the distinct short- and long-term goals.

pnkfelix added a commit to pnkfelix/rust that referenced this issue Oct 16, 2018

Make us error consistently in issue rust-lang#21232, to fix rust-lang…
…#54986.

Treat attempt to partially intialize local `l` as uses of a `mut` in `let mut l;`, to fix rust-lang#54499.
@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 16, 2018

(We did talk about this at the last meeting; removing the I-nominated tag.)

@pnkfelix pnkfelix removed the I-nominated label Oct 16, 2018

bors added a commit that referenced this issue Oct 17, 2018

Auto merge of #54941 - pnkfelix:issue-21232-reject-partial-reinit, r=…
…nikomatsakis

reject partial init and reinit of uninitialized data

Reject partial initialization of uninitialized structured types (i.e. structs and tuples) and also reject partial *reinitialization* of such types.

Fix #54986

Fix #54499

cc #21232
@pnkfelix

This comment has been minimized.

Copy link
Member Author

commented Oct 17, 2018

#54986 has landed.

#54987 covers the longer term goal of supporting partial initialization of structs.

So this issue (#21232) can be closed.

@pnkfelix pnkfelix closed this Oct 17, 2018

davidtwco added a commit to davidtwco/rust that referenced this issue Nov 3, 2018

Unions reinitialized after assignment into field.
This commit makes two changes:

First, it updates the dataflow builder to add an init for the place
containing a union if there is an assignment into the field of
that union.

Second, it stops a "use of uninitialized" error occuring when there is an
assignment into the field of an uninitialized union that was previously
initialized. Making this assignment would re-initialize the union, as
tested in `src/test/ui/borrowck/borrowck-union-move-assign.nll.stderr`.
The check for previous initialization ensures that we do not start
supporting partial initialization yet (cc rust-lang#21232, rust-lang#54499, rust-lang#54986).

bors added a commit that referenced this issue Nov 11, 2018

Auto merge of #55657 - davidtwco:issue-55651, r=pnkfelix
NLL Diagnostic Review 3: Unions not reinitialized after assignment into field

Fixes #55651, #55652.

This PR makes two changes:

First, it updates the dataflow builder to add an init for the place
containing a union if there is an assignment into the field of
that union.

Second, it stops a "use of uninitialized" error occuring when there is an
assignment into the field of an uninitialized union that was previously
initialized. Making this assignment would re-initialize the union, as
tested in `src/test/ui/borrowck/borrowck-union-move-assign.nll.stderr`.
The check for previous initialization ensures that we do not start
supporting partial initialization yet (cc #21232, #54499, #54986).

This PR also fixes #55652 which was marked as requiring investigation
as the changes in this PR add an error that was previously missing
(and mentioned in the review comments) and confirms that the error
that was present is correct and a result of earlier partial
initialization changes in NLL.

r? @pnkfelix (due to earlier work with partial initialization)
cc @nikomatsakis

bors added a commit that referenced this issue Nov 11, 2018

Auto merge of #55657 - davidtwco:issue-55651, r=pnkfelix
NLL Diagnostic Review 3: Unions not reinitialized after assignment into field

Fixes #55651, #55652.

This PR makes two changes:

First, it updates the dataflow builder to add an init for the place
containing a union if there is an assignment into the field of
that union.

Second, it stops a "use of uninitialized" error occuring when there is an
assignment into the field of an uninitialized union that was previously
initialized. Making this assignment would re-initialize the union, as
tested in `src/test/ui/borrowck/borrowck-union-move-assign.nll.stderr`.
The check for previous initialization ensures that we do not start
supporting partial initialization yet (cc #21232, #54499, #54986).

This PR also fixes #55652 which was marked as requiring investigation
as the changes in this PR add an error that was previously missing
(and mentioned in the review comments) and confirms that the error
that was present is correct and a result of earlier partial
initialization changes in NLL.

r? @pnkfelix (due to earlier work with partial initialization)
cc @nikomatsakis
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.