-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Implement volatile_composites
lint
#15686
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
Conversation
rustbot has assigned @samueltardieu. Use |
No changes for 1d7c1af |
This comment has been minimized.
This comment has been minimized.
7b7b0ee
to
14ae176
Compare
This comment has been minimized.
This comment has been minimized.
ec5febf
to
fc17293
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution. It is in a good shape already, but I have some questions.
Do not hesitate to add extra tests, especially if my comments were wrong, in order to distinguish between the cases you thought of when the code was more complete than what I suggest.
Also, I would like to have more doc comments on functions, so that it is easier to read.
/// } | ||
/// } | ||
/// ``` | ||
#[clippy::version = "1.91.0"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[clippy::version = "1.91.0"] | |
#[clippy::version = "1.92.0"] |
// NonNull::{read_volatile,write_volatile} | ||
|
||
// primitive type: | ||
// unit, [iu]{8,16,32,64,128?}, f{32,64}, thin pointer, usize, isize, bool, char |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about f16
and f128
?
What are those comments anyway? Were those notes you took during development that need to be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'll clean these up. In practice this set of types is whatever Ty::is_primitive
returns true for, which includes f16
and f128
.
// Zero-sized types are intrinsically safe to use volatile on since they won't | ||
// actually generate *any* loads or stores. But this is also used to skip zero | ||
// fields of #[repr(transparent)] structures. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use doc comments to comment out functions? This makes it easier to navigate when later editing the code, as IDEs will render them properly.
// actually generate *any* loads or stores. But this is also used to skip zero | ||
// fields of #[repr(transparent)] structures. | ||
fn is_zero_sized_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
if let Ok(ty) = cx.tcx.try_normalize_erasing_regions(cx.typing_env(), ty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't layout_of()
already normalize the type? Are there cases that would not be covered by the following?
cx.tcx
.layout_of(cx.typing_env().as_query_input(ty))
.is_ok_and(|layout| layout.layout.is_zst())
If there are, it would be great to add them as tests.
if let ty::RawPtr(_inner, _) = ty.kind() { | ||
ty.pointee_metadata_ty_or_projection(cx.tcx).is_unit() | ||
} else { | ||
false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there cases of real ()
metadata? Won't the following be equivalent?
matches!(ty.kind(), ty::RawPtr(inner, _) if inner.has_trivial_sizedness(cx.tcx, SizedTraitKind::Sized))
if let ty::Adt(adt_def, _args) = ty.kind() | ||
&& adt_def.is_enum() | ||
&& adt_def.repr().inhibit_struct_field_reordering() | ||
{ | ||
adt_def.is_payloadfree() | ||
} else { | ||
false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I prefer the shorter equivalent form but it is a matter of taste.
if let ty::Adt(adt_def, _args) = ty.kind() | |
&& adt_def.is_enum() | |
&& adt_def.repr().inhibit_struct_field_reordering() | |
{ | |
adt_def.is_payloadfree() | |
} else { | |
false | |
} | |
ty.ty_adt_def().is_some_and(|adt_def| { | |
adt_def.is_enum() && adt_def.repr().inhibit_struct_field_reordering() && adt_def.is_payloadfree() | |
}) |
|
||
// We can't know about a generic type, so just let it pass to avoid noise | ||
fn is_generic<'tcx>(_cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
ty.flags().intersects(ty::TypeFlags::HAS_PARAM) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ty.flags()
will be removed at some point. Can't you use ty.has_param()
? Or ty.has_non_region_param()
if a generic lifetime wouldn't change the lint (along with a test with raw pointer on a type with a lifetime)?
Also, this function is so short that you could probably remove it and replace its call by its content.
// Raw pointers | ||
ty::RawPtr(innerty, _) => report_volatile_safe(cx, expr, *innerty), | ||
// std::ptr::NonNull | ||
ty::Adt(adt_def, args) if cx.tcx.is_diagnostic_item(sym::NonNull, adt_def.did()) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: we have a utility for this check in clippy_utils::ty
:
ty::Adt(adt_def, args) if cx.tcx.is_diagnostic_item(sym::NonNull, adt_def.did()) => { | |
_ if is_type_diagnostic_item(cx, self_ty, sym::NonNull) => { |
cx.tcx.get_diagnostic_name(def_id), | ||
Some(sym::ptr_read_volatile | sym::ptr_write_volatile) | ||
) | ||
&& let ty::RawPtr(ptrty, _) = cx.typeck_results().expr_ty(arg_ptr).kind() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you use expr_ty_adjusted()
here? The lint currently doesn't catch:
let mut s = String::from("foo");
unsafe {
std::ptr::write_volatile(&mut s, String::from("bar"));
//~^ volatile_composites
}
while it probably should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch. I wasn't sure what an "adjustment" was for types.
@samueltardieu Thanks very much for your feedback. I'll address your comments shortly. This is the first time I've delved into clippy's internals so I was drawing on existing examples and guesswork for a lot of this change, so your pointers are much appreciated. |
I addressed review comments in a separate commit, but I can fold them if it all looks OK. |
340ae34
to
ec9f7c0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is starting to look good.
I'd like to see two more tests, they may be covered by other paths but it would be more explicit to me:
struct Empty;
(&mut Empty as *mut Empty).write_volatile(Empty); // OK
(0xdead as *mut Wrapper<Empty>).write_volatile(Wrapper((), Empty, ())); // OK
Also, regarding primitive types, it looks like this lint should cover non-atomic volatile operations on non-atomic primitive types. Recommending in the description to use primitive types might be misleading, and maybe the name of the lint should be something like non_atomic_volatile_operations
or something like that.
cx.tcx | ||
.layout_of(cx.typing_env().as_query_input(ty)) | ||
.is_ok_and(|layout| layout.is_zst()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can probably further simplify this:
cx.tcx | |
.layout_of(cx.typing_env().as_query_input(ty)) | |
.is_ok_and(|layout| layout.is_zst()) | |
cx.layout_of(ty).is_ok_and(|layout| layout.is_zst()) |
|
||
/// Top-level predicate for whether a type is volatile-safe or not. | ||
fn is_volatile_safe_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
ty.is_primitive() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all primitive types volatile safe? I would think that there is no chance that 32-bit architectures support atomic read and write to u128
data for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid using the term "atomic" with respect to volatile - volatile does not make any atomic guarantees (certainly not in the std::sync::atomic
sense). Volatile is a bridge to memory outside the Rust abstract machine memory model, so at the Rust language level it has pretty weak and machine-specific guarantees. So there's no guarantee that a volatile operation on, say, a u32
will be a single memory load/store, except as a machine-specific implementation detail (though it would be very surprising if it weren't true on any >=32 bit cpu). So the fact that u128 probably won't turn into a single load/store is fine.
The specific problem this lint is intended to highlight is that there's no consensus about what this means for a composite type like a struct. Some people believe that write_volatile
should treat it as a single entity, and so the compiler is OK to write multiple fields in a single store if that makes sense. Others believe that it should treat each field as a separate entity which is stored as an independent object. The documentation is ambiguous about this, and makes no guarantees either way, and can change from compiler (or llvm) version to version. So code which depends on one or the other interpretation may work today but stop tomorrow.
Even as it is, if the target address has normal memory semantics (eg a memory region shared with a different OS process or CPU complex), it may not make a difference one way or the other so this lint wouldn't apply regardless of what type is used.
We could consider a separate lint which flags volatile read/writes for non-composite types which are not accessed with a single load/store. But that would be highly target-specific and ultimately depends on the backend's (ie typically llvm, but could be cranelift or gcc) precise implementation, which could change from version to version, and also depend on opt-level and other flags. So that sounds like it would be hard to get right.
Ultimately volatile read/write are already unsafe
so the user already bears a high burden of proof to make it safe. This is just one aspect of it, and this lint is intended to help them get it right, but obviously can't be aware of all the factors in play.
139eeda
to
9e95933
Compare
I simplified that ZST predicate and added a test with
See my reply to your other comment, though the TL;DR is that I think we should avoid the use of "atomic" in this context, and whether a primitive volatile read/write results in a single load/store is a separate (though related) issue, which is much harder to check for. |
9e95933
to
5a1391d
Compare
(Also remove use of "atomic" in comment in exactly the sense I say we shouldn't be using it.) |
@samueltardieu ping? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please squash the changes?
I hadn't noticed that they were ready to be reviewed because the S-waiting-on-author
label was set. You can comment with @rustbot ready
or request a review using GitHub's interface to set the S-waiting-on-review
label instead.
I've opened a FCP thread on Zulip. @rustbot label S-final-comment-period -Swaiting-on-author |
Volatile reads and writes to non-primitive types are not well-defined, and can cause problems. See rust-lang#15529 for more details.
5a1391d
to
1d7c1af
Compare
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
Squashed and rebased to current master. |
Volatile reads and writes to non-primitive types are not well-defined, and can cause problems.
Fixes #15529
changelog: [
volatile_composites
]: Lint when read/write_volatile is used on composite types(structs, arrays, etc) as their semantics are not well defined.