-
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
Merged
+496
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
use clippy_utils::diagnostics::span_lint; | ||
use clippy_utils::sym; | ||
use clippy_utils::ty::is_type_diagnostic_item; | ||
use rustc_hir::{Expr, ExprKind}; | ||
use rustc_lint::{LateContext, LateLintPass}; | ||
use rustc_middle::ty::layout::LayoutOf; | ||
use rustc_middle::ty::{self, Ty, TypeVisitableExt}; | ||
use rustc_session::declare_lint_pass; | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// | ||
/// This lint warns when volatile load/store operations | ||
/// (`write_volatile`/`read_volatile`) are applied to composite types. | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// Volatile operations are typically used with memory mapped IO devices, | ||
/// where the precise number and ordering of load and store instructions is | ||
/// important because they can have side effects. This is well defined for | ||
/// primitive types like `u32`, but less well defined for structures and | ||
/// other composite types. In practice it's implementation defined, and the | ||
/// behavior can be rustc-version dependent. | ||
/// | ||
/// As a result, code should only apply `write_volatile`/`read_volatile` to | ||
/// primitive types to be fully well-defined. | ||
/// | ||
/// ### Example | ||
/// ```no_run | ||
/// struct MyDevice { | ||
/// addr: usize, | ||
/// count: usize | ||
/// } | ||
/// | ||
/// fn start_device(device: *mut MyDevice, addr: usize, count: usize) { | ||
/// unsafe { | ||
/// device.write_volatile(MyDevice { addr, count }); | ||
/// } | ||
/// } | ||
/// ``` | ||
/// Instead, operate on each primtive field individually: | ||
/// ```no_run | ||
/// struct MyDevice { | ||
/// addr: usize, | ||
/// count: usize | ||
/// } | ||
/// | ||
/// fn start_device(device: *mut MyDevice, addr: usize, count: usize) { | ||
/// unsafe { | ||
/// (&raw mut (*device).addr).write_volatile(addr); | ||
/// (&raw mut (*device).count).write_volatile(count); | ||
/// } | ||
/// } | ||
/// ``` | ||
#[clippy::version = "1.92.0"] | ||
pub VOLATILE_COMPOSITES, | ||
nursery, | ||
"warn about volatile read/write applied to composite types" | ||
} | ||
declare_lint_pass!(VolatileComposites => [VOLATILE_COMPOSITES]); | ||
|
||
/// 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-sized | ||
/// fields of `#[repr(transparent)]` structures. | ||
fn is_zero_sized_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
cx.layout_of(ty).is_ok_and(|layout| layout.is_zst()) | ||
} | ||
|
||
/// A thin raw pointer or reference. | ||
fn is_narrow_ptr<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
match ty.kind() { | ||
ty::RawPtr(inner, _) | ty::Ref(_, inner, _) => inner.has_trivial_sizedness(cx.tcx, ty::SizedTraitKind::Sized), | ||
_ => false, | ||
} | ||
} | ||
|
||
/// Enum with some fixed representation and no data-carrying variants. | ||
fn is_enum_repr_c<'tcx>(_cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
ty.ty_adt_def().is_some_and(|adt_def| { | ||
adt_def.is_enum() && adt_def.repr().inhibit_struct_field_reordering() && adt_def.is_payloadfree() | ||
}) | ||
} | ||
|
||
/// `#[repr(transparent)]` structures are also OK if the only non-zero | ||
/// sized field contains a volatile-safe type. | ||
fn is_struct_repr_transparent<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
if let ty::Adt(adt_def, args) = ty.kind() | ||
&& adt_def.is_struct() | ||
&& adt_def.repr().transparent() | ||
&& let [fieldty] = adt_def | ||
.all_fields() | ||
.filter_map(|field| { | ||
let fty = field.ty(cx.tcx, args); | ||
if is_zero_sized_ty(cx, fty) { None } else { Some(fty) } | ||
}) | ||
.collect::<Vec<_>>() | ||
.as_slice() | ||
{ | ||
is_volatile_safe_ty(cx, *fieldty) | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
/// SIMD can be useful to get larger single loads/stores, though this is still | ||
/// pretty machine-dependent. | ||
fn is_simd_repr<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
if let ty::Adt(adt_def, _args) = ty.kind() | ||
&& adt_def.is_struct() | ||
&& adt_def.repr().simd() | ||
{ | ||
let (_size, simdty) = ty.simd_size_and_type(cx.tcx); | ||
is_volatile_safe_ty(cx, simdty) | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
/// 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() | ||
|| is_narrow_ptr(cx, ty) | ||
|| is_zero_sized_ty(cx, ty) | ||
|| is_enum_repr_c(cx, ty) | ||
|| is_simd_repr(cx, ty) | ||
|| is_struct_repr_transparent(cx, ty) | ||
// We can't know about a generic type, so just let it pass to avoid noise | ||
|| ty.has_non_region_param() | ||
} | ||
|
||
/// Print diagnostic for volatile read/write on non-volatile-safe types. | ||
fn report_volatile_safe<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>, ty: Ty<'tcx>) { | ||
if !is_volatile_safe_ty(cx, ty) { | ||
span_lint( | ||
cx, | ||
VOLATILE_COMPOSITES, | ||
expr.span, | ||
format!("type `{ty}` is not volatile-compatible"), | ||
); | ||
} | ||
} | ||
|
||
impl<'tcx> LateLintPass<'tcx> for VolatileComposites { | ||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { | ||
// Check our expr is calling a method with pattern matching | ||
match expr.kind { | ||
// Look for method calls to `write_volatile`/`read_volatile`, which | ||
// apply to both raw pointers and std::ptr::NonNull. | ||
ExprKind::MethodCall(name, self_arg, _, _) | ||
if matches!(name.ident.name, sym::read_volatile | sym::write_volatile) => | ||
jsgf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
let self_ty = cx.typeck_results().expr_ty(self_arg); | ||
match self_ty.kind() { | ||
// Raw pointers | ||
ty::RawPtr(innerty, _) => report_volatile_safe(cx, expr, *innerty), | ||
// std::ptr::NonNull | ||
ty::Adt(_, args) if is_type_diagnostic_item(cx, self_ty, sym::NonNull) => { | ||
report_volatile_safe(cx, expr, args.type_at(0)); | ||
}, | ||
_ => (), | ||
} | ||
}, | ||
|
||
// Also plain function calls to std::ptr::{read,write}_volatile | ||
ExprKind::Call(func, [arg_ptr, ..]) => { | ||
if let ExprKind::Path(ref qpath) = func.kind | ||
&& let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() | ||
&& matches!( | ||
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_adjusted(arg_ptr).kind() | ||
{ | ||
report_volatile_safe(cx, expr, *ptrty); | ||
} | ||
}, | ||
_ => {}, | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.Uh oh!
There was an error while loading. Please reload this page.
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, au32
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.