Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6798,6 +6798,7 @@ Released 2018-09-13
[`vec_resize_to_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#vec_resize_to_zero
[`verbose_bit_mask`]: https://rust-lang.github.io/rust-clippy/master/index.html#verbose_bit_mask
[`verbose_file_reads`]: https://rust-lang.github.io/rust-clippy/master/index.html#verbose_file_reads
[`volatile_composites`]: https://rust-lang.github.io/rust-clippy/master/index.html#volatile_composites
[`vtable_address_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#vtable_address_comparisons
[`waker_clone_wake`]: https://rust-lang.github.io/rust-clippy/master/index.html#waker_clone_wake
[`while_float`]: https://rust-lang.github.io/rust-clippy/master/index.html#while_float
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::visibility::NEEDLESS_PUB_SELF_INFO,
crate::visibility::PUB_WITHOUT_SHORTHAND_INFO,
crate::visibility::PUB_WITH_SHORTHAND_INFO,
crate::volatile_composites::VOLATILE_COMPOSITES_INFO,
crate::wildcard_imports::ENUM_GLOB_USE_INFO,
crate::wildcard_imports::WILDCARD_IMPORTS_INFO,
crate::write::PRINTLN_EMPTY_STRING_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ mod useless_conversion;
mod vec;
mod vec_init_then_push;
mod visibility;
mod volatile_composites;
mod wildcard_imports;
mod write;
mod zero_div_zero;
Expand Down Expand Up @@ -831,5 +832,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(infallible_try_from::InfallibleTryFrom));
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
// add lints here, do not remove this comment, it's used in `new_lint`
}
180 changes: 180 additions & 0 deletions clippy_lints/src/volatile_composites.rs
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()
Copy link
Member

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.

Copy link
Contributor Author

@jsgf jsgf Sep 21, 2025

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.

|| 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) =>
{
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);
}
},
_ => {},
}
}
}
2 changes: 2 additions & 0 deletions clippy_utils/src/sym.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ generate! {
read_to_end,
read_to_string,
read_unaligned,
read_volatile,
redundant_imports,
redundant_pub_crate,
regex,
Expand Down Expand Up @@ -373,6 +374,7 @@ generate! {
wrapping_offset,
write,
write_unaligned,
write_volatile,
writeln,
zip,
}
Loading