-
Notifications
You must be signed in to change notification settings - Fork 13k
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
Miscompilation where binding only some fields leaks the others #90752
Comments
Note that the miscompilation goes away if you change rust/library/core/src/option.rs Line 1211 in e5cfe84
|
Also, the miscompilation is in MIR, not LLVM, because Miri also fails to run the destructor. |
Very slightly smaller (I removed the fields in struct S;
impl Drop for S {
fn drop(&mut self) {
println!("dropping");
}
}
enum E {
A,
B((S, S)),
}
fn main() {
let mut foo = Some(E::A);
match foo {
Some(E::A) => (),
Some(E::B(_)) | None => return,
}
foo.insert(E::B((S, S)));
match foo {
Some(E::B((x, _))) => {}
_ => {},
};
} |
Oh strangely this is not related to the |
Here's a version without |
A quick manual bisection on godbolt shows that this regresses in Rust 1.43 (is correct in Rust 1.42). @rustbot label: regression-from-stable-to-stable |
Bisection reveals that this issue is introduced in #68528. I think the issue is that precise_enum_drop_elaboration didn't re-mark all variants as possibly active after the mutable reference to the enum is used. The miscompilation goes away with cc @ecstatic-morse |
Thanks for the detailed bug report. I think I know what the root cause is, but it's not a trivial fix. I'll work on this later today. |
The root causeHere's my preferred minimization, using an indirect write instead of struct S(i32);
impl Drop for S {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let mut foo = None;
match foo {
None => (),
_ => return,
}
// Here we know that `foo` is `None`, otherwise we would have returned above.
// Overwrite `foo` indirectly. `foo` is no longer `None`, but we fail to update our
// initialization data to reflect this.
*(&mut foo) = Some((S(0), S(1)));
// Move from `(foo as Some).0` (despite the fact that we think it is uninitialized),
// creating a drop flag for `(foo as Some).0` and making the subsequent
// drop of `foo` an open one.
match foo {
Some((_x, _)) => {
} // `_x` is dropped: "dropping 0"
_ => {}
}
// `foo` is dropped, but it is an "open" drop, so the move paths associated with its
//`Some` variants are still marked as uninitialized and the `Drop` is eliminated.
} I was a bit surprised that the second As @nbdd0121 suggests, it's the write through the mutable borrow that causes the problem. However, once we started marking inactive enum variants as uninitialized, it became possible to have a mutable reference that can update an uninitialized place in safe code, which is what happened here. A quick fixTo fix the memory leak in the near term, it should suffice to modify let mut foo = None;
let pfoo = &mut foo; // Mutable borrow here
match foo {
Some(_) => return,
None => (),
}
// `Some` variants are marked as uninitialized, despite the fact that a mutable borrow is outstanding.
*pfoo = Some(...); // Reinitialize `Some` variants, without creating a new mutable borrow. However, there may be ways to circumvent this. For example, converting Perhaps there are even more cases where this fix falls short. I've opened a PR that implements this (#90788) so I can discuss it further with the compiler team. A long-term solutionThe real mistake here is conflating the active variant of a local with its initializedness. In the long-term, we should use a bespoke dataflow analysis to track the possible active variants of an enum, and incorporate those results into drop elaboration. The approach in #68582 was taken because it required the smallest number of changes to drop elaboration, a complex part of the compiler whose original author is no longer active and where it is easy to introduce subtle miscompilations. One safeguard that would have caught this is to check the results of Even if the quick fix is acceptable for now, I think we should pursue something more robust going forward. |
Assigning priority as discussed in the Zulip thread of the Prioritization Working Group. @rustbot label -I-prioritize +P-critical |
…iser Mark places as initialized when mutably borrowed Fixes the example in rust-lang#90752, but does not handle some corner cases involving raw pointers and unsafe. See [this comment](rust-lang#90752 (comment)) for more information, or the second test. Although I talked about both `MaybeUninitializedPlaces` and `MaybeInitializedPlaces` in rust-lang#90752, this PR only changes the latter. That's because "maybe uninitialized" is the conservative choice, and marking them as definitely initialized (`!maybe_uninitialized`) when a mutable borrow is created could lead to problems if `addr_of_mut` to an uninitialized local is allowed. Additionally, places cannot become uninitialized via a mutable reference, so if a place is definitely initialized, taking a mutable reference to it should not change that. I think it's correct to ignore interior mutability as nbdd0121 suggests below. Their analysis doesn't work inside of `core::cell`, which *does* have access to `UnsafeCell`'s field, but that won't be an issue unless we explicitly instantiate one with an `enum` within that module. r? `@wesleywiser`
FYI This is addressed by PR #90788, which will be part of the Rust 1.58 release (stable in six weeks). |
Checking progress, @mk12 did you have a chance to test against the new stable 1.58 if it solves the issue? thanks |
@apiraino The example I gave in my first comment now behaves as expected on the Rust playground with 1.58.1 stable. I'm fairly sure this solves the original Fuchsia bug, but it's difficult to reproduce that because a workaround was used at the time and that code has changed a lot since. |
I'm going to close this issue as we think it's solved. In case it can be reopened or follow-up issues can be created |
I found a bug where Rust leaks values (never calls
drop
). This is the root cause of a memory leak we saw in a Fuchsia test, caught by LeakSanitizer: https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=87961.This is as far as I could minimize it:
Expected output:
Actual output:
It leaked
S(2)
. If you change(x, _)
to(_, x)
in the second match statement, it leaksS(1)
instead. If you bind neither(_, _)
or both(x, y)
, then both get dropped as expected.If you use
Box<u8>
instead ofS
, you can detect a memory leak using LeakSanitizer or Valgrind.Meta
I reproduced this on stable:
And on nightly:
The text was updated successfully, but these errors were encountered: