-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathvolatile_composites.rs
More file actions
181 lines (170 loc) · 6.62 KB
/
volatile_composites.rs
File metadata and controls
181 lines (170 loc) · 6.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use clippy_utils::diagnostics::span_lint;
use clippy_utils::res::MaybeDef;
use clippy_utils::sym;
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) =>
{
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 self_ty.is_diag_item(cx, 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);
}
},
_ => {},
}
}
}