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

Initial impl of raw_assign_to_drop #13866

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

lukaslueg
Copy link
Contributor

Fixes #4294

The lint simply checks all assignments via unsafe pointers where to dereferenced-type has drop-glue.

I'm somewhat unsure about what to call this thing - is it assign_raw_ptr_using_drop, raw_assign_drop, dropped_assign_raw, ... ?

Although some of the tests involve &mut as *mut, the lint does not make efforts to filter out situations where the raw pointer is derived from a known-safe source. The general assumption is that if we have a raw pointer at at all, and assign to the place behind the pointer, then all safety bets are off anyway (otherwise, one could have assigned via &mut to begin with).

changelog: [raw_assign_to_drop]: Initial impl

@rustbot
Copy link
Collaborator

rustbot commented Dec 22, 2024

r? @Centri3

rustbot has assigned @Centri3.
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 Dec 22, 2024
Copy link
Member

@Centri3 Centri3 left a comment

Choose a reason for hiding this comment

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

Looks fine to me beyond the suggestion nit

--> tests/ui/raw_assign_to_drop.rs:10:11
|
LL | (*r, *r) = ("foo".to_owned(), "bar".to_owned());
| ^ this place may be uninitialized, causing Undefined Behavior when the destructor executes
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should use place in suggestions.

/// ```no_run
/// unsafe fn foo(oldvalue: *mut String) {
/// // Direct assignment always executes `String`'s destructor on `oldvalue`
/// *oldvalue = "New Value".to_owned();
Copy link
Contributor

Choose a reason for hiding this comment

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

You should use an unsafe block inside the function, even if this is an unsafe function, as this example will warn in edition 2024:

warning[E0133]: dereference of raw pointer is unsafe and requires unsafe block
 --> t.rs:4:5
  |
4 |     *oldvalue = "New Value".to_owned();
  |     ^^^^^^^^^ dereference of raw pointer
  |
  = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/unsafe-op-in-unsafe-fn.html>
  = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
note: an unsafe function restricts its caller, but its body is safe by default

Comment on lines +854 to +857
/// Use `std::ptr::write()` to overwrite a value without executing the destructor.
///
/// Use `std::ptr::drop_in_place()` to conditionally execute the destructor if you are
/// sure that the place contains an initialized value.
Copy link
Contributor

Choose a reason for hiding this comment

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

Right now, this text does not clearly acknowledge that “the value is always initialized (and so the code is correct)” is a possible case. I worry that people will take it as “plain assignments are always bad style”, and possibly rewrite correct code into code that forgets instead of dropping (or add needless oldvalue_is_initialized flags, or just take away “sheesh, Rust has things that are broken by default”). I think it would be significantly better if the text and examples gives a concrete recommendation for the case where the pointer is initialized.

The obvious concrete recommendation to make is “allow/expect the lint”, but in some cases, converting back to safe &mut could be better, allowing the unsafe to be more narrowly scoped — I’m particularly thinking of cases where raw pointers are being used to perform borrow splitting that can’t be checked by the borrow checker, where it makes sense to convert to &mut as soon as the splitting is done, but before the actual writes happen.

Copy link
Member

Choose a reason for hiding this comment

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

My initial impression when I read through this and #4294 was that the recommended way to resolve this is to simply always use ptr::write() (+ ptr::drop_in_place if what the user wrote is really what they want - the user can then addditonally justify that dropping is indeed the correct thing to do by writing a safety comment which can be enforced by other lints, and is then no longer hidden behind the = operator). So if the value is initialized, write ptr.drop_in_place(); ptr.write(value);.

This is also how I understood the help messages:

   = help: use `std::ptr::write()` to overwrite a (possibly uninitialized) place
   = help: use `std::ptr::drop_in_place()` to drop the previous value if such value exists

I feel like recommending to allow the lint in the case that the code is correct goes against what the help messages are saying?

Copy link
Contributor

@kpreid kpreid Jan 12, 2025

Choose a reason for hiding this comment

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

I have two thoughts on this, which are:

  1. Verbosity is not necessarily clarity. Splitting a single assignment into separate drop_in_place() and write() calls:

    • Creates a hazard, in that if they end up with some other code inserted in between them, that code is executing with a sort of broken invariant that might be overlooked:
      ptr::drop_in_place(self.ptr);
      ptr::write(self.ptr, self.compute_new_value()); // compute_new_value could see a deinited *self.ptr!
    • Makes it look like something more esoteric than an ordinary Rust assignment to an initialized place in safe code.

    I could get behind “you should convert the pointer to &mut before assigning”, which makes it clear that the invariants of &mut (initialization, exclusive access) apply, but “implement the two parts of assignment yourself just so you’re being explicit” feels like imposing a lot of mental cost for the sake of “code that is doing something else than what this code is doing might be doing something wrong”.

  2. If this lint is proposing never assigning through a raw pointer, that feels rather like it is saying “don't use this part of the language at all!” recommendation, which — if Clippy is doing something like that outside of restriction lints, then it feels like something has gone wrong at the language-design level. Rust shouldn’t have (non-deprecated) features that are always wrong to use.

    And perhaps this feature should be deprecated, but in that case, it's time to talk to T-lang, not just add a lint.

@lukaslueg
Copy link
Contributor Author

How about

/// If the code can guarantee that the value being replaced during assignment is initialized
/// in all cases, it is recommended to
/// * silence this warning by adding a `#[expect(raw_assign_to_drop)]`-attribute
/// * make liberal use of SAFETY-comments e.g. around declaration of this unsafe pointer,
///   reminding readers that this unsafe pointer *must* contain an initialized value.

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.

Lint suggestion: write to raw pointer with drop glue
6 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy