Skip to content

Do not suggest to use implicit DerefMut on ManuallyDrop reached through unions #14387

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

samueltardieu
Copy link
Member

@samueltardieu samueltardieu commented Mar 10, 2025

This requires making the deref_addrof lint a late lint instead of an early lint to check for types.

changelog: [deref_addrof]: do not suggest implicit DerefMut on ManuallyDrop union fields

Fix #14386

@rustbot
Copy link
Collaborator

rustbot commented Mar 10, 2025

r? @Alexendoo

rustbot has assigned @Alexendoo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Mar 10, 2025
Comment on lines 100 to 109
// Lint, as `Copy` fields in unions are ok.
(*(&raw mut a.with_padding)) = [1; size_of::<DataWithPadding>()];
//~^ deref_addrof
*(*(&raw mut a.x)) = 0;
//~^ deref_addrof
(*(&raw mut a.y)).0 = 0;
//~^ deref_addrof
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error looks to be specific to ManuallyDrop rather than Copy, e.g. this is an error even though everything is Copy

#[derive(Copy, Clone)]
struct Data {
    num: u64,
}

union U {
    data: ManuallyDrop<Data>,
}

fn x(mut a: U) {
    unsafe {
        a.data.num = 3;
    }
}

*a.x = 0; doesn't hit it because it doesn't use DerefMut

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error looks to be specific to ManuallyDrop rather than Copy, e.g. this is an error even though everything is Copy

Indeed. And it requires going through the parent exprs to check if a ManuallyDrop is mutably dereferenced. I have reused some code from reference.rs and shared it in clippy_utils::ty.

@samueltardieu samueltardieu changed the title Do not suggest to use implicit DerefMut on non-Copy union fields Do not suggest to use implicit DerefMut on ManuallyDrop union fields Mar 11, 2025
@samueltardieu
Copy link
Member Author

@Alexendoo Anything missing on my side?

Comment on lines 108 to 121
/// Check if `expr` is an access to an union field which contains a dereferenced
/// `ManuallyDrop<_>` entity.
fn is_manually_drop_union_field(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let typeck = cx.typeck_results();
if let ExprKind::Field(parent, _) = expr.kind
&& typeck.expr_ty_adjusted(parent).is_union()
{
for (_, node) in cx.tcx.hir_parent_iter(expr.hir_id) {
if let Node::Expr(expr) = node {
if adjust_derefs_manually_drop(typeck.expr_adjustments(expr), typeck.expr_ty(expr)) {
return true;
}
} else {
break;
}
}
}
false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this lints

use std::mem::ManuallyDrop;

#[derive(Clone, Copy)]
struct B {
    md: ManuallyDrop<[u8; 4]>
}

union U {
    b: B,
}

fn x(mut u: U) {
    unsafe {
        (*&mut u.b.md)[3] = 1;
        //     ~~~~~~ `expr`
    }
}

Currently we only check if u.b is a union, but we need to keep going while we still see field/index expressions since it doesn't have to directly in the union

The parent iter I believe can be replaced with is_manually_drop([type of expr])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check this, thanks for the review. @rustbot author

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the cases you mention, and some similar ones, are no longer linted.

However, I am not sure that

The parent iter I believe can be replaced with is_manually_drop([type of expr])

holds, as not all ManuallyDrop are equal here: **&mut a.prim = 0 is linted and fixed as *a.prim = 0, as there is an explicit deref left. However, (*&mut a.data).num = 42 is currently no longer linted, as a.data.num = 42 would be an implicit deref, but we could suggest to keep the explicit dereference instead, as in (*a.data).num = 42.

I'll think more about this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not as simple as keeping the reference and removing the dereference in some case where the implicit dereference happens later in the expression:

(*&mut a.tup).0.num = 42;

should be rewritten as

(*a.tup.0).num = 42;

if we want to remove the &num. That would require moving the explicit dereference to the point where the implicit DerefMut would take place, so this case should not be linted.

I've added a new commit which reinstates those cases and suggests the proper fix. I may squash the two commits after the review.

One thing I have not checked is if it is really wise to keep linting in macros, as the lint is currently doing. If a macro is used with a ManuallyDrop field reached through a union, the lint may incorrectly suggest fixing the macro in cases where it works fine, which would generate an error in other cases. I would be in favor of disabling the lint in macros, even if we have false negatives, this would be better than false positives. Thoughts?

@rustbot ready

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@RalfJung RalfJung May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd that this would error:

union U<T> { x:(), f: ManuallyDrop<T> }

fn main() {
    let mut u : U<(Vec<i32>,)> = U { x: () };
    let r = u;
    unsafe { r.f.0 = Vec::new() }; // uninitialized `Vec` being dropped
}

and the 2nd example you gave would not.

But I don't think I had any involvement with that error, and from a soundness perspective everything is fine here -- there's an unsafe block and you wrote your code wrong, so you got UB. So I'm the wrong person to ask here, sorry; auto-(de)ref is far outside my realm.

I'd suggest to open a rustc issue clearly describing the problem; your example above isn't very self-explaining and it took me a bit of experimentation to even figure out what you were likely talking about.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look, I pinged since I saw you authored rust-lang/rust#75584 but I didn't catch the date being that long ago

I'll write up an issue for it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess I did write that code then, and entirely forgot about it since then.^^ But I was never very comfortable in rustc_typeck.

@rustbot

This comment has been minimized.

@samueltardieu
Copy link
Member Author

Rebased

@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label May 6, 2025
@rustbot
Copy link
Collaborator

rustbot commented May 6, 2025

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@rustbot rustbot added the S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) label May 6, 2025
@samueltardieu samueltardieu changed the title Do not suggest to use implicit DerefMut on ManuallyDrop union fields Do not suggest to use implicit DerefMut on ManuallyDrop reached through unions May 7, 2025
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels May 7, 2025
@rustbot

This comment has been minimized.

This is necessary in order to use type analysis in later commits.
@samueltardieu
Copy link
Member Author

Rebased

@samueltardieu
Copy link
Member Author

Ping @Alexendoo

@samueltardieu
Copy link
Member Author

This has been ready for months now.

r? clippy

@rustbot rustbot assigned blyxyas and unassigned Alexendoo Jul 28, 2025
@blyxyas
Copy link
Member

blyxyas commented Jul 31, 2025

I'll make reviewing this a priority. A review should be ready soon.

Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What an interesting bug, I have some questions and some refactorings! Btw, if the original lint handled macros correctly, is there a hard reason to aovid them now, or are you just safeguarding the lint for future bugs?

let typeck = cx.typeck_results();
if is_reached_through_union(cx, addrof_target) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't query typeck results if is_reached_through_union can return false.

Comment on lines +103 to +104
for (idx, id) in std::iter::once(expr_id)
.chain(cx.tcx.hir_parent_id_iter(expr_id))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at hir_parent_id_iter, I think that it already includes the current expr_id in the traversal, doesn't it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so: the .next() ends with

        self.current_id = parent_id;
        Some(parent_id)

So it never yields current_id (which is the initialization value).

.chain(cx.tcx.hir_parent_id_iter(expr_id))
.enumerate()
{
if let Node::Expr(expr) = cx.tcx.hir_node(id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of getting the node, we can use hir_parent_iter directly and avoid this step. As .map is lazy and won't compute not-traversed nodes, it won't have be a performance impact.

Copy link
Member Author

@samueltardieu samueltardieu Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make the change, but I'll still have to use hir_node() for the initial node.

In fact, hir_parent_iter() will only complicate the code, as I need to call hir_node() for the initial node, and hir_parent_iter() is not more efficient as it calls hir_node() on any obtained HirId.

Comment on lines 54 to 63
let sugg = Sugg::hir_with_applicability(cx, addrof_target, "_", &mut applicability);

let mut generate_snippet = |pattern: &str| {
#[expect(clippy::cast_possible_truncation)]
macro_source.rfind(pattern).map(|pattern_pos| {
let rpos = pattern_pos + pattern.len();
let span_after_ref = e.span.with_lo(BytePos(e.span.lo().0 + rpos as u32));
let span = trim_leading_whitespaces(span_after_ref);
snippet_with_applicability(cx, span, "_", &mut applicability)
})
};
// If this expression is an explicit `DerefMut` of a `ManuallyDrop` reached through a
// union, we may remove the reference if we are at the point where the implicit
// dereference would take place. Otherwise, we should not lint.
let sugg = match is_manually_drop_through_union(cx, e.hir_id, addrof_target) {
ManuallyDropThroughUnion::Directly => sugg.deref(),
ManuallyDropThroughUnion::Indirect => return,
ManuallyDropThroughUnion::No => sugg,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hir_with_applicability is pretty costly, this can be refactored to avoid computing the suggestion in the return case.

What about something like this?

Suggested change
let sugg = Sugg::hir_with_applicability(cx, addrof_target, "_", &mut applicability);
let mut generate_snippet = |pattern: &str| {
#[expect(clippy::cast_possible_truncation)]
macro_source.rfind(pattern).map(|pattern_pos| {
let rpos = pattern_pos + pattern.len();
let span_after_ref = e.span.with_lo(BytePos(e.span.lo().0 + rpos as u32));
let span = trim_leading_whitespaces(span_after_ref);
snippet_with_applicability(cx, span, "_", &mut applicability)
})
};
// If this expression is an explicit `DerefMut` of a `ManuallyDrop` reached through a
// union, we may remove the reference if we are at the point where the implicit
// dereference would take place. Otherwise, we should not lint.
let sugg = match is_manually_drop_through_union(cx, e.hir_id, addrof_target) {
ManuallyDropThroughUnion::Directly => sugg.deref(),
ManuallyDropThroughUnion::Indirect => return,
ManuallyDropThroughUnion::No => sugg,
};
// If this expression is an explicit `DerefMut` of a `ManuallyDrop` reached through a
// union, we may remove the reference if we are at the point where the implicit
// dereference would take place. Otherwise, we should not lint.
let manually_drop_through_union = is_manually_drop_through_union(cx, e.hir_id, addrof_target);
let sugg = if manually_drop_through_union == ManuallyDropThoughUnion::Indirect {
return;
} else {
let raw_sugg = Sugg::hir_with_applicability(cx, addrof_target, "_", &mut applicability);
if manually_drop_through_union == ManuallyDropThroughUnion::Directly {
raw_sugg.deref()
}
raw_sugg
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll use a closure to avoid early evaluation, it allows me to keep the match which I find clear.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Jul 31, 2025
Suggesting to remove `*&` or `*&mut` in a macro may be incorrect, as it
would require tracking all possible macro usages. In some cases, it is
not possible to remove the dereference or the reference.

For example, in the following code, the `drmut!()` macro will be linted
against while called as `drmut!(d)`, and the suggestion would be to
remove the `*&mut`. That would make `drmut!(u.data).num = 1` invalid,
as a `ManuallyDrop` object coming through a union cannot be implicitely
dereferenced through `DerefMut`.

```rust
use std::mem::ManuallyDrop;

#[derive(Copy, Clone)]
struct Data {
    num: u64,
}

union Union {
    data: ManuallyDrop<Data>,
}

macro_rules! drmut {
    ($e:expr) => { *&mut $e };
}

fn f(mut u: Union, mut d: Data) {
    unsafe {
        drmut!(u.data).num = 1;
    }
    drmut!(d).num = 1;
}
```
@samueltardieu
Copy link
Member Author

What an interesting bug, I have some questions and some refactorings! Btw, if the original lint handled macros correctly, is there a hard reason to aovid them now, or are you just safeguarding the lint for future bugs?

I don't think the original lint handled macros correctly in presence of ManuallyDrop reached through unions (as it didn't handle any code correctly in such a case, macro or not), and it is hard to make it correct with macros once it is fixed for non-macro code. There is a description in the last commit for why we don't lint macros, I'll copy it here:

Suggesting to remove *& or *&mut in a macro may be incorrect, as it
would require tracking all possible macro usages. In some cases, it is
not possible to remove the dereference or the reference.

For example, in the following code, the drmut!() macro will be linted
against while called as drmut!(d), and the suggestion would be to
remove the *&mut. That would make drmut!(u.data).num = 1 invalid,
as a ManuallyDrop object coming through a union cannot be implicitely
dereferenced through DerefMut.

use std::mem::ManuallyDrop;

#[derive(Copy, Clone)]
struct Data {
    num: u64,
}

union Union {
    data: ManuallyDrop<Data>,
}

macro_rules! drmut {
    ($e:expr) => { *&mut $e };
}

fn f(mut u: Union, mut d: Data) {
    unsafe {
        drmut!(u.data).num = 1;
    }
    drmut!(d).num = 1;
}

@samueltardieu samueltardieu requested a review from blyxyas July 31, 2025 14:00
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Jul 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Clippy suggests broken code when modifying union fields behind ManuallyDrop
5 participants