Skip to content

Commit bb5cfee

Browse files
committed
Add one_unsafe_op_per_block lint
1 parent 07a7603 commit bb5cfee

File tree

6 files changed

+495
-0
lines changed

6 files changed

+495
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4434,6 +4434,7 @@ Released 2018-09-13
44344434
[`obfuscated_if_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#obfuscated_if_else
44354435
[`octal_escapes`]: https://rust-lang.github.io/rust-clippy/master/index.html#octal_escapes
44364436
[`ok_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#ok_expect
4437+
[`one_unsafe_op_per_block`]: https://rust-lang.github.io/rust-clippy/master/index.html#one_unsafe_op_per_block
44374438
[`only_used_in_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#only_used_in_recursion
44384439
[`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
44394440
[`option_and_then_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_and_then_some

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
454454
crate::non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY_INFO,
455455
crate::nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES_INFO,
456456
crate::octal_escapes::OCTAL_ESCAPES_INFO,
457+
crate::one_unsafe_op_per_block::ONE_UNSAFE_OP_PER_BLOCK_INFO,
457458
crate::only_used_in_recursion::ONLY_USED_IN_RECURSION_INFO,
458459
crate::operators::ABSURD_EXTREME_COMPARISONS_INFO,
459460
crate::operators::ARITHMETIC_SIDE_EFFECTS_INFO,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ mod non_octal_unix_permissions;
223223
mod non_send_fields_in_send_ty;
224224
mod nonstandard_macro_braces;
225225
mod octal_escapes;
226+
mod one_unsafe_op_per_block;
226227
mod only_used_in_recursion;
227228
mod operators;
228229
mod option_env_unwrap;
@@ -908,6 +909,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
908909
store.register_late_pass(|_| Box::new(fn_null_check::FnNullCheck));
909910
store.register_late_pass(|_| Box::new(permissions_set_readonly_false::PermissionsSetReadonlyFalse));
910911
store.register_late_pass(|_| Box::new(size_of_ref::SizeOfRef));
912+
store.register_late_pass(|_| Box::new(one_unsafe_op_per_block::OneUnsafeOpPerBlock));
911913
// add lints here, do not remove this comment, it's used in `new_lint`
912914
}
913915

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use hir::{
3+
def::{DefKind, Res},
4+
BlockCheckMode, ExprKind, Local, QPath, StmtKind, UnOp, Unsafety,
5+
};
6+
use rustc_ast::Mutability;
7+
use rustc_hir as hir;
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::{declare_lint_pass, declare_tool_lint};
10+
use rustc_span::Span;
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Checks for `unsafe` blocks that contain more than one unsafe operation.
15+
///
16+
/// ### Why is this bad?
17+
/// Combined with `undocumented_unsafe_blocks`,
18+
/// this lint ensures that each unsafe operation must be independently justified.
19+
/// Combined with `unused_unsafe`, this lint also ensures
20+
/// elimination of unnecessary unsafe blocks through refactoring.
21+
///
22+
/// ### Example
23+
/// ```rust
24+
/// /// Reads a `char` from the given pointer.
25+
/// ///
26+
/// /// # Safety
27+
/// ///
28+
/// /// `ptr` must point to four consecutive, initialized bytes which
29+
/// /// form a valid `char` when interpreted in the native byte order.
30+
/// fn read_char(ptr: *const u8) -> char {
31+
/// // SAFETY: The caller has guaranteed that the value pointed
32+
/// // to by `bytes` is a valid `char`.
33+
/// unsafe { char::from_u32_unchecked(*ptr.cast::<u32>()) }
34+
/// }
35+
/// ```
36+
/// Use instead:
37+
/// ```rust
38+
/// /// Reads a `char` from the given pointer.
39+
/// ///
40+
/// /// # Safety
41+
/// ///
42+
/// /// - `ptr` must be 4-byte aligned, point to four consecutive
43+
/// /// initialized bytes, and be valid for reads of 4 bytes.
44+
/// /// - The bytes pointed to by `ptr` must represent a valid
45+
/// /// `char` when interpreted in the native byte order.
46+
/// fn read_char(ptr: *const u8) -> char {
47+
/// // SAFETY: `ptr` is 4-byte aligned, points to four consecutive
48+
/// // initialized bytes, and is valid for reads of 4 bytes.
49+
/// let int_value = unsafe { *ptr.cast::<u32>() };
50+
///
51+
/// // SAFETY: The caller has guaranteed that the four bytes
52+
/// // pointed to by `bytes` represent a valid `char`.
53+
/// unsafe { char::from_u32_unchecked(int_value) }
54+
/// }
55+
/// ```
56+
#[clippy::version = "1.68.0"]
57+
pub ONE_UNSAFE_OP_PER_BLOCK,
58+
restriction,
59+
"more than one unsafe operation per `unsafe` block"
60+
}
61+
declare_lint_pass!(OneUnsafeOpPerBlock => [ONE_UNSAFE_OP_PER_BLOCK]);
62+
63+
impl<'tcx> LateLintPass<'tcx> for OneUnsafeOpPerBlock {
64+
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &hir::Block<'_>) {
65+
let mut unsafe_ops = vec![];
66+
collect_block(cx, block, &mut unsafe_ops);
67+
if unsafe_ops.len() > 1 {
68+
span_lint_and_then(
69+
cx,
70+
ONE_UNSAFE_OP_PER_BLOCK,
71+
block.span,
72+
&format!(
73+
"this `unsafe` block contains {} unsafe operations, expected only one",
74+
unsafe_ops.len()
75+
),
76+
|diag| {
77+
for (msg, span) in unsafe_ops {
78+
diag.span_note(span, msg);
79+
}
80+
},
81+
);
82+
}
83+
}
84+
}
85+
86+
fn collect_block(cx: &LateContext<'_>, block: &hir::Block<'_>, unsafe_ops: &mut Vec<(&'static str, Span)>) {
87+
if let BlockCheckMode::UnsafeBlock(_) = block.rules {
88+
if let Some(expr) = block.expr {
89+
collect_unsafe_exprs(cx, expr, unsafe_ops);
90+
}
91+
for stmt in block.stmts {
92+
match stmt.kind {
93+
StmtKind::Local(Local { init, els: r#else, .. }) => {
94+
if let Some(init) = init {
95+
collect_unsafe_exprs(cx, init, unsafe_ops);
96+
}
97+
if let Some(r#else) = r#else {
98+
collect_block(cx, r#else, unsafe_ops);
99+
}
100+
},
101+
StmtKind::Expr(e) | StmtKind::Semi(e) => collect_unsafe_exprs(cx, e, unsafe_ops),
102+
StmtKind::Item(_) => {},
103+
}
104+
}
105+
}
106+
}
107+
108+
// This whole function is practically one big `match` block
109+
#[expect(
110+
clippy::too_many_lines,
111+
reason = "long match means long function, not much you can do here"
112+
)]
113+
fn collect_unsafe_exprs(cx: &LateContext<'_>, expr: &hir::Expr<'_>, unsafe_ops: &mut Vec<(&'static str, Span)>) {
114+
let expr = expr.peel_drop_temps();
115+
116+
match expr.kind {
117+
// Relevant matches (contain `unsafe_ops.push`)
118+
ExprKind::InlineAsm(_) => unsafe_ops.push(("inline assembly used here", expr.span)),
119+
120+
ExprKind::Field(e, _) => {
121+
collect_unsafe_exprs(cx, e, unsafe_ops);
122+
if cx.typeck_results().expr_ty(e).is_union() {
123+
unsafe_ops.push(("union field access occurs here", expr.span));
124+
}
125+
},
126+
127+
ExprKind::Path(QPath::Resolved(_, hir::Path { res: Res::Def(DefKind::Static(Mutability::Mut), _), ..})) => {
128+
unsafe_ops.push(("access of a mutable static occurs here", expr.span));
129+
},
130+
131+
ExprKind::Unary(UnOp::Deref, e) if cx.typeck_results().expr_ty_adjusted(e).is_unsafe_ptr() => {
132+
unsafe_ops.push(("raw pointer dereference occurs here", expr.span));
133+
collect_unsafe_exprs(cx, e, unsafe_ops);
134+
},
135+
136+
ExprKind::Call(path_expr, args) => {
137+
args.iter().for_each(|arg| collect_unsafe_exprs(cx, arg, unsafe_ops));
138+
139+
match path_expr.kind {
140+
ExprKind::Path(QPath::Resolved(_, hir::Path { res: Res::Def(kind, def_id), ..}))
141+
if kind.is_fn_like() => {
142+
let sig = cx.tcx.bound_fn_sig(*def_id);
143+
if sig.0.unsafety() == Unsafety::Unsafe {
144+
unsafe_ops.push(("unsafe function call occurs here", expr.span));
145+
}
146+
},
147+
148+
ExprKind::Path(QPath::TypeRelative(..)) => {
149+
if let Some(sig) = cx
150+
.typeck_results()
151+
.type_dependent_def_id(path_expr.hir_id)
152+
.map(|def_id| cx.tcx.bound_fn_sig(def_id))
153+
{
154+
if sig.0.unsafety() == Unsafety::Unsafe {
155+
unsafe_ops.push(("unsafe function call occurs here", expr.span));
156+
}
157+
}
158+
},
159+
160+
_ => {}
161+
}
162+
},
163+
164+
ExprKind::MethodCall(_, recv, args, _) => {
165+
collect_unsafe_exprs(cx, recv, unsafe_ops);
166+
args.iter().for_each(|arg| collect_unsafe_exprs(cx, arg, unsafe_ops));
167+
if let Some(sig) = cx
168+
.typeck_results()
169+
.type_dependent_def_id(expr.hir_id)
170+
.map(|def_id| cx.tcx.bound_fn_sig(def_id))
171+
{
172+
if sig.0.unsafety() == Unsafety::Unsafe {
173+
unsafe_ops.push(("unsafe method call occurs here", expr.span));
174+
}
175+
}
176+
},
177+
178+
ExprKind::AssignOp(_, lhs, rhs) | ExprKind::Assign(lhs, rhs, _) => {
179+
collect_unsafe_exprs(cx, rhs, unsafe_ops);
180+
if matches!(
181+
lhs.kind,
182+
ExprKind::Path(QPath::Resolved(_, hir::Path { res: Res::Def(DefKind::Static(Mutability::Mut), _), .. }))
183+
) {
184+
unsafe_ops.push(("modification of a mutable static occurs here", expr.span));
185+
}
186+
},
187+
188+
// Not-so-relevant matches (mostly delegating)
189+
ExprKind::Box(e)
190+
| ExprKind::Ret(Some(e))
191+
| ExprKind::Repeat(e, _)
192+
| ExprKind::Cast(e, _)
193+
| ExprKind::Type(e, _)
194+
| ExprKind::Yield(e, _)
195+
| ExprKind::Break(_, Some(e))
196+
| ExprKind::Unary(_, e)
197+
| ExprKind::AddrOf(_, _, e) => collect_unsafe_exprs(cx, e, unsafe_ops),
198+
199+
ExprKind::Array(exprs) => exprs.iter().for_each(|e| collect_unsafe_exprs(cx, e, unsafe_ops)),
200+
201+
ExprKind::Binary(_, first, second) => {
202+
collect_unsafe_exprs(cx, first, unsafe_ops);
203+
collect_unsafe_exprs(cx, second, unsafe_ops);
204+
},
205+
206+
ExprKind::Struct(_, exprs, base) => {
207+
if let Some(base) = base {
208+
collect_unsafe_exprs(cx, base, unsafe_ops);
209+
}
210+
exprs.iter().for_each(|e| collect_unsafe_exprs(cx, e.expr, unsafe_ops));
211+
},
212+
213+
ExprKind::Tup(exprs) => exprs.iter().for_each(|e| collect_unsafe_exprs(cx, e, unsafe_ops)),
214+
215+
ExprKind::Match(base, arms, _) => {
216+
collect_unsafe_exprs(cx, base, unsafe_ops);
217+
for arm in arms.iter() {
218+
collect_unsafe_exprs(cx, arm.body, unsafe_ops);
219+
if let Some(ref guard) = arm.guard {
220+
collect_unsafe_exprs(cx, guard.body(), unsafe_ops);
221+
}
222+
}
223+
},
224+
225+
ExprKind::Let(r#let) => collect_unsafe_exprs(cx, r#let.init, unsafe_ops),
226+
227+
ExprKind::If(base, r#if, r#else) => {
228+
collect_unsafe_exprs(cx, base, unsafe_ops);
229+
collect_unsafe_exprs(cx, r#if, unsafe_ops);
230+
if let Some(r#else) = r#else {
231+
collect_unsafe_exprs(cx, r#else, unsafe_ops);
232+
}
233+
},
234+
235+
ExprKind::Index(recv, idx) => {
236+
collect_unsafe_exprs(cx, recv, unsafe_ops);
237+
collect_unsafe_exprs(cx, idx, unsafe_ops);
238+
},
239+
240+
ExprKind::ConstBlock(_)
241+
| ExprKind::Path(_)
242+
| ExprKind::Break(_, _)
243+
| ExprKind::Ret(None)
244+
| ExprKind::Continue(_)
245+
| ExprKind::DropTemps(_)
246+
| ExprKind::Closure(_)
247+
| ExprKind::Lit(_)
248+
| ExprKind::Loop(..)
249+
| ExprKind::Block(..) // that (maybe) unsafe block will be checked later by another `check_block` pass
250+
| ExprKind::Err => {},
251+
}
252+
}

tests/ui/one_unsafe_op_per_block.rs

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#![allow(unused)]
2+
#![allow(deref_nullptr)]
3+
#![allow(clippy::unnecessary_operation)]
4+
#![allow(clippy::drop_copy)]
5+
#![warn(clippy::one_unsafe_op_per_block)]
6+
7+
use core::arch::asm;
8+
9+
fn raw_ptr() -> *const () {
10+
core::ptr::null()
11+
}
12+
13+
unsafe fn not_very_safe() {}
14+
15+
struct Sample;
16+
17+
impl Sample {
18+
unsafe fn not_very_safe(&self) {}
19+
}
20+
21+
#[allow(non_upper_case_globals)]
22+
const sample: Sample = Sample;
23+
24+
union U {
25+
i: i32,
26+
u: u32,
27+
}
28+
29+
static mut STATIC: i32 = 0;
30+
31+
fn test1() {
32+
unsafe {
33+
STATIC += 1;
34+
not_very_safe();
35+
}
36+
}
37+
38+
fn test2() {
39+
let u = U { i: 0 };
40+
41+
unsafe {
42+
drop(u.u);
43+
*raw_ptr();
44+
}
45+
}
46+
47+
fn test3() {
48+
unsafe {
49+
asm!("nop");
50+
sample.not_very_safe();
51+
STATIC = 0;
52+
}
53+
}
54+
55+
fn test_all() {
56+
let u = U { i: 0 };
57+
unsafe {
58+
drop(u.u);
59+
drop(STATIC);
60+
sample.not_very_safe();
61+
not_very_safe();
62+
*raw_ptr();
63+
asm!("nop");
64+
}
65+
}
66+
67+
// no lint
68+
fn correct1() {
69+
unsafe {
70+
STATIC += 1;
71+
}
72+
}
73+
74+
// no lint
75+
fn correct2() {
76+
unsafe {
77+
STATIC += 1;
78+
}
79+
80+
unsafe {
81+
*raw_ptr();
82+
}
83+
}
84+
85+
// no lint
86+
fn correct3() {
87+
let u = U { u: 0 };
88+
89+
unsafe {
90+
not_very_safe();
91+
}
92+
93+
unsafe {
94+
drop(u.i);
95+
}
96+
}
97+
98+
// tests from the issue (https://github.com/rust-lang/rust-clippy/issues/10064)
99+
100+
unsafe fn read_char_bad(ptr: *const u8) -> char {
101+
unsafe { char::from_u32_unchecked(*ptr.cast::<u32>()) }
102+
}
103+
104+
// no lint
105+
unsafe fn read_char_good(ptr: *const u8) -> char {
106+
let int_value = unsafe { *ptr.cast::<u32>() };
107+
unsafe { core::char::from_u32_unchecked(int_value) }
108+
}
109+
110+
fn main() {}

0 commit comments

Comments
 (0)