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
Assignees
Labels
A-NLL Area: Non Lexical Lifetimes (NLL) A-typesystem Area: The type system C-feature-request Category: A feature request, i.e: not implemented / a PR. NLL-complete Working towards the "valid code works" goal P-high High priority

Comments

@pnkfelix
Copy link
Member

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
Copy link
Member Author

(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.)

@kmcallister kmcallister added the A-typesystem Area: The type system label Jan 16, 2015
@mbrubeck
Copy link
Contributor

mbrubeck 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
Copy link
Member Author

pnkfelix 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
Copy link
Member Author

pnkfelix commented Mar 3, 2015

triage: P-backcompat-lang (1.0 beta)

just trying to accommodate highfive...

@nikomatsakis
Copy link
Contributor

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
Copy link
Contributor

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

@nikomatsakis nikomatsakis added the I-needs-decision Issue: In need of a decision. label Mar 5, 2015
@mbrubeck
Copy link
Contributor

mbrubeck 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
Copy link
Member Author

pnkfelix 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
Copy link
Member Author

pnkfelix commented Mar 5, 2015

triage: P-low ()

@rust-highfive rust-highfive added P-low Low priority and removed P-backcompat-lang labels Mar 5, 2015
@pnkfelix
Copy link
Member Author

pnkfelix 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
Copy link
Member Author

pnkfelix 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
Copy link
Contributor

@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
Copy link

logannc 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
Copy link
Member

huonw commented Jun 30, 2015

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

@gnzlbg
Copy link
Contributor

gnzlbg 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
Copy link
Member

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 Relevant to the language team, which will review and decide on the PR/issue. label Jan 11, 2016
@pnkfelix pnkfelix added P-high High priority I-nominated and removed I-needs-decision Issue: In need of a decision. T-lang Relevant to the language team, which will review and decide on the PR/issue. P-low Low priority I-nominated labels Oct 4, 2018
@pnkfelix
Copy link
Member Author

pnkfelix 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
Copy link
Member Author

pnkfelix 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
Copy link
Member Author

pnkfelix commented Oct 9, 2018

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

@pnkfelix
Copy link
Member Author

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
…#54986.

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

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

bors added a commit that referenced this issue Oct 17, 2018
…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
Copy link
Member Author

#54986 has landed.

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

So this issue (#21232) can be closed.

davidtwco added a commit to davidtwco/rust that referenced this issue Nov 3, 2018
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
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
Labels
A-NLL Area: Non Lexical Lifetimes (NLL) A-typesystem Area: The type system C-feature-request Category: A feature request, i.e: not implemented / a PR. NLL-complete Working towards the "valid code works" goal P-high High priority
Projects
None yet
Development

No branches or pull requests