Skip to content

Commit b3145fe

Browse files
committed
Auto merge of #10099 - Niki4tap:null_fn_lints, r=llogiq
Null fn lints Adds lints to check for code, that assumes nullable `fn()`. ### Lint examples: `transmute_null_to_fn`: ```rust error: transmuting a known null pointer into a function pointer --> $DIR/transmute_null_to_fn.rs:9:23 | LL | let _: fn() = std::mem::transmute(std::ptr::null::<()>()); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this transmute results in undefined behavior | = help: try wrapping your function pointer type in `Option<T>` instead, and using `None` as a null pointer value ``` `fn_null_check`: ```rust error: function pointer assumed to be nullable, even though it isn't --> $DIR/fn_null_check.rs:13:8 | LL | if (fn_ptr as *mut ()).is_null() {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value ``` Closes #1644 --- changelog: Improvement: [`transmuting_null`]: Now detects `const` pointers to all types [#10099](#10099) changelog: New lint: [`transmute_null_to_fn`] [#10099](#10099) changelog: New lint: [`fn_null_check`] [#10099](#10099) <!-- changelog_checked (This is just a flag for me, please don't add it manually) -->
2 parents 8a6aca3 + 691df70 commit b3145fe

12 files changed

+328
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4203,6 +4203,7 @@ Released 2018-09-13
42034203
[`float_cmp_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#float_cmp_const
42044204
[`float_equality_without_abs`]: https://rust-lang.github.io/rust-clippy/master/index.html#float_equality_without_abs
42054205
[`fn_address_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#fn_address_comparisons
4206+
[`fn_null_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#fn_null_check
42064207
[`fn_params_excessive_bools`]: https://rust-lang.github.io/rust-clippy/master/index.html#fn_params_excessive_bools
42074208
[`fn_to_numeric_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#fn_to_numeric_cast
42084209
[`fn_to_numeric_cast_any`]: https://rust-lang.github.io/rust-clippy/master/index.html#fn_to_numeric_cast_any
@@ -4590,6 +4591,7 @@ Released 2018-09-13
45904591
[`transmute_int_to_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_int_to_bool
45914592
[`transmute_int_to_char`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_int_to_char
45924593
[`transmute_int_to_float`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_int_to_float
4594+
[`transmute_null_to_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_null_to_fn
45934595
[`transmute_num_to_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_num_to_bytes
45944596
[`transmute_ptr_to_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_ptr_to_ptr
45954597
[`transmute_ptr_to_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#transmute_ptr_to_ref

clippy_lints/src/declared_lints.rs

+2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
161161
crate::float_literal::LOSSY_FLOAT_LITERAL_INFO,
162162
crate::floating_point_arithmetic::IMPRECISE_FLOPS_INFO,
163163
crate::floating_point_arithmetic::SUBOPTIMAL_FLOPS_INFO,
164+
crate::fn_null_check::FN_NULL_CHECK_INFO,
164165
crate::format::USELESS_FORMAT_INFO,
165166
crate::format_args::FORMAT_IN_FORMAT_ARGS_INFO,
166167
crate::format_args::TO_STRING_IN_FORMAT_ARGS_INFO,
@@ -568,6 +569,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
568569
crate::transmute::TRANSMUTE_INT_TO_BOOL_INFO,
569570
crate::transmute::TRANSMUTE_INT_TO_CHAR_INFO,
570571
crate::transmute::TRANSMUTE_INT_TO_FLOAT_INFO,
572+
crate::transmute::TRANSMUTE_NULL_TO_FN_INFO,
571573
crate::transmute::TRANSMUTE_NUM_TO_BYTES_INFO,
572574
crate::transmute::TRANSMUTE_PTR_TO_PTR_INFO,
573575
crate::transmute::TRANSMUTE_PTR_TO_REF_INFO,

clippy_lints/src/fn_null_check.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use clippy_utils::consts::{constant, Constant};
2+
use clippy_utils::diagnostics::span_lint_and_help;
3+
use clippy_utils::{is_integer_literal, is_path_diagnostic_item};
4+
use rustc_hir::{BinOpKind, Expr, ExprKind, TyKind};
5+
use rustc_lint::{LateContext, LateLintPass};
6+
use rustc_session::{declare_lint_pass, declare_tool_lint};
7+
use rustc_span::sym;
8+
9+
declare_clippy_lint! {
10+
/// ### What it does
11+
/// Checks for comparing a function pointer to null.
12+
///
13+
/// ### Why is this bad?
14+
/// Function pointers are assumed to not be null.
15+
///
16+
/// ### Example
17+
/// ```rust,ignore
18+
/// let fn_ptr: fn() = /* somehow obtained nullable function pointer */
19+
///
20+
/// if (fn_ptr as *const ()).is_null() { ... }
21+
/// ```
22+
/// Use instead:
23+
/// ```rust,ignore
24+
/// let fn_ptr: Option<fn()> = /* somehow obtained nullable function pointer */
25+
///
26+
/// if fn_ptr.is_none() { ... }
27+
/// ```
28+
#[clippy::version = "1.67.0"]
29+
pub FN_NULL_CHECK,
30+
correctness,
31+
"`fn()` type assumed to be nullable"
32+
}
33+
declare_lint_pass!(FnNullCheck => [FN_NULL_CHECK]);
34+
35+
fn lint_expr(cx: &LateContext<'_>, expr: &Expr<'_>) {
36+
span_lint_and_help(
37+
cx,
38+
FN_NULL_CHECK,
39+
expr.span,
40+
"function pointer assumed to be nullable, even though it isn't",
41+
None,
42+
"try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value",
43+
);
44+
}
45+
46+
fn is_fn_ptr_cast(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
47+
if let ExprKind::Cast(cast_expr, cast_ty) = expr.kind
48+
&& let TyKind::Ptr(_) = cast_ty.kind
49+
{
50+
cx.typeck_results().expr_ty_adjusted(cast_expr).is_fn()
51+
} else {
52+
false
53+
}
54+
}
55+
56+
impl<'tcx> LateLintPass<'tcx> for FnNullCheck {
57+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
58+
match expr.kind {
59+
// Catching:
60+
// (fn_ptr as *<const/mut> <ty>).is_null()
61+
ExprKind::MethodCall(method_name, receiver, _, _)
62+
if method_name.ident.as_str() == "is_null" && is_fn_ptr_cast(cx, receiver) =>
63+
{
64+
lint_expr(cx, expr);
65+
},
66+
67+
ExprKind::Binary(op, left, right) if matches!(op.node, BinOpKind::Eq) => {
68+
let to_check: &Expr<'_>;
69+
if is_fn_ptr_cast(cx, left) {
70+
to_check = right;
71+
} else if is_fn_ptr_cast(cx, right) {
72+
to_check = left;
73+
} else {
74+
return;
75+
}
76+
77+
match to_check.kind {
78+
// Catching:
79+
// (fn_ptr as *<const/mut> <ty>) == (0 as <ty>)
80+
ExprKind::Cast(cast_expr, _) if is_integer_literal(cast_expr, 0) => {
81+
lint_expr(cx, expr);
82+
},
83+
84+
// Catching:
85+
// (fn_ptr as *<const/mut> <ty>) == std::ptr::null()
86+
ExprKind::Call(func, []) if is_path_diagnostic_item(cx, func, sym::ptr_null) => {
87+
lint_expr(cx, expr);
88+
},
89+
90+
// Catching:
91+
// (fn_ptr as *<const/mut> <ty>) == <const that evaluates to null_ptr>
92+
_ if matches!(
93+
constant(cx, cx.typeck_results(), to_check),
94+
Some((Constant::RawPtr(0), _))
95+
) =>
96+
{
97+
lint_expr(cx, expr);
98+
},
99+
100+
_ => {},
101+
}
102+
},
103+
_ => {},
104+
}
105+
}
106+
}

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ mod explicit_write;
125125
mod fallible_impl_from;
126126
mod float_literal;
127127
mod floating_point_arithmetic;
128+
mod fn_null_check;
128129
mod format;
129130
mod format_args;
130131
mod format_impl;
@@ -902,6 +903,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
902903
store.register_late_pass(|_| Box::new(suspicious_xor_used_as_pow::ConfusingXorAndPow));
903904
store.register_late_pass(move |_| Box::new(manual_is_ascii_check::ManualIsAsciiCheck::new(msrv())));
904905
store.register_late_pass(|_| Box::new(semicolon_block::SemicolonBlock));
906+
store.register_late_pass(|_| Box::new(fn_null_check::FnNullCheck));
905907
// add lints here, do not remove this comment, it's used in `new_lint`
906908
}
907909

clippy_lints/src/transmute/mod.rs

+31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod transmute_float_to_int;
33
mod transmute_int_to_bool;
44
mod transmute_int_to_char;
55
mod transmute_int_to_float;
6+
mod transmute_null_to_fn;
67
mod transmute_num_to_bytes;
78
mod transmute_ptr_to_ptr;
89
mod transmute_ptr_to_ref;
@@ -409,6 +410,34 @@ declare_clippy_lint! {
409410
"transmutes from a null pointer to a reference, which is undefined behavior"
410411
}
411412

413+
declare_clippy_lint! {
414+
/// ### What it does
415+
/// Checks for null function pointer creation through transmute.
416+
///
417+
/// ### Why is this bad?
418+
/// Creating a null function pointer is undefined behavior.
419+
///
420+
/// More info: https://doc.rust-lang.org/nomicon/ffi.html#the-nullable-pointer-optimization
421+
///
422+
/// ### Known problems
423+
/// Not all cases can be detected at the moment of this writing.
424+
/// For example, variables which hold a null pointer and are then fed to a `transmute`
425+
/// call, aren't detectable yet.
426+
///
427+
/// ### Example
428+
/// ```rust
429+
/// let null_fn: fn() = unsafe { std::mem::transmute( std::ptr::null::<()>() ) };
430+
/// ```
431+
/// Use instead:
432+
/// ```rust
433+
/// let null_fn: Option<fn()> = None;
434+
/// ```
435+
#[clippy::version = "1.67.0"]
436+
pub TRANSMUTE_NULL_TO_FN,
437+
correctness,
438+
"transmute results in a null function pointer, which is undefined behavior"
439+
}
440+
412441
pub struct Transmute {
413442
msrv: Msrv,
414443
}
@@ -428,6 +457,7 @@ impl_lint_pass!(Transmute => [
428457
TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS,
429458
TRANSMUTE_UNDEFINED_REPR,
430459
TRANSMUTING_NULL,
460+
TRANSMUTE_NULL_TO_FN,
431461
]);
432462
impl Transmute {
433463
#[must_use]
@@ -461,6 +491,7 @@ impl<'tcx> LateLintPass<'tcx> for Transmute {
461491
let linted = wrong_transmute::check(cx, e, from_ty, to_ty)
462492
| crosspointer_transmute::check(cx, e, from_ty, to_ty)
463493
| transmuting_null::check(cx, e, arg, to_ty)
494+
| transmute_null_to_fn::check(cx, e, arg, to_ty)
464495
| transmute_ptr_to_ref::check(cx, e, from_ty, to_ty, arg, path, &self.msrv)
465496
| transmute_int_to_char::check(cx, e, from_ty, to_ty, arg, const_context)
466497
| transmute_ref_to_ref::check(cx, e, from_ty, to_ty, arg, const_context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use clippy_utils::consts::{constant, Constant};
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::{is_integer_literal, is_path_diagnostic_item};
4+
use rustc_hir::{Expr, ExprKind};
5+
use rustc_lint::LateContext;
6+
use rustc_middle::ty::Ty;
7+
use rustc_span::symbol::sym;
8+
9+
use super::TRANSMUTE_NULL_TO_FN;
10+
11+
fn lint_expr(cx: &LateContext<'_>, expr: &Expr<'_>) {
12+
span_lint_and_then(
13+
cx,
14+
TRANSMUTE_NULL_TO_FN,
15+
expr.span,
16+
"transmuting a known null pointer into a function pointer",
17+
|diag| {
18+
diag.span_label(expr.span, "this transmute results in undefined behavior");
19+
diag.help(
20+
"try wrapping your function pointer type in `Option<T>` instead, and using `None` as a null pointer value"
21+
);
22+
},
23+
);
24+
}
25+
26+
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, arg: &'tcx Expr<'_>, to_ty: Ty<'tcx>) -> bool {
27+
if !to_ty.is_fn() {
28+
return false;
29+
}
30+
31+
match arg.kind {
32+
// Catching:
33+
// transmute over constants that resolve to `null`.
34+
ExprKind::Path(ref _qpath)
35+
if matches!(constant(cx, cx.typeck_results(), arg), Some((Constant::RawPtr(0), _))) =>
36+
{
37+
lint_expr(cx, expr);
38+
true
39+
},
40+
41+
// Catching:
42+
// `std::mem::transmute(0 as *const i32)`
43+
ExprKind::Cast(inner_expr, _cast_ty) if is_integer_literal(inner_expr, 0) => {
44+
lint_expr(cx, expr);
45+
true
46+
},
47+
48+
// Catching:
49+
// `std::mem::transmute(std::ptr::null::<i32>())`
50+
ExprKind::Call(func1, []) if is_path_diagnostic_item(cx, func1, sym::ptr_null) => {
51+
lint_expr(cx, expr);
52+
true
53+
},
54+
55+
_ => {
56+
// FIXME:
57+
// Also catch transmutations of variables which are known nulls.
58+
// To do this, MIR const propagation seems to be the better tool.
59+
// Whenever MIR const prop routines are more developed, this will
60+
// become available. As of this writing (25/03/19) it is not yet.
61+
false
62+
},
63+
}
64+
}

clippy_lints/src/transmute/transmuting_null.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, arg: &'t
1818
// Catching transmute over constants that resolve to `null`.
1919
let mut const_eval_context = constant_context(cx, cx.typeck_results());
2020
if let ExprKind::Path(ref _qpath) = arg.kind &&
21-
let Some(Constant::RawPtr(x)) = const_eval_context.expr(arg) &&
22-
x == 0
21+
let Some(Constant::RawPtr(0)) = const_eval_context.expr(arg)
2322
{
2423
span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG);
2524
return true;

clippy_utils/src/consts.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -620,12 +620,7 @@ pub fn miri_to_const<'tcx>(tcx: TyCtxt<'tcx>, result: mir::ConstantKind<'tcx>) -
620620
ty::Float(FloatTy::F64) => Some(Constant::F64(f64::from_bits(
621621
int.try_into().expect("invalid f64 bit representation"),
622622
))),
623-
ty::RawPtr(type_and_mut) => {
624-
if let ty::Uint(_) = type_and_mut.ty.kind() {
625-
return Some(Constant::RawPtr(int.assert_bits(int.size())));
626-
}
627-
None
628-
},
623+
ty::RawPtr(_) => Some(Constant::RawPtr(int.assert_bits(int.size()))),
629624
// FIXME: implement other conversions.
630625
_ => None,
631626
}

tests/ui/fn_null_check.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#![allow(unused)]
2+
#![warn(clippy::fn_null_check)]
3+
#![allow(clippy::cmp_null)]
4+
#![allow(clippy::ptr_eq)]
5+
#![allow(clippy::zero_ptr)]
6+
7+
pub const ZPTR: *const () = 0 as *const _;
8+
pub const NOT_ZPTR: *const () = 1 as *const _;
9+
10+
fn main() {
11+
let fn_ptr = main;
12+
13+
if (fn_ptr as *mut ()).is_null() {}
14+
if (fn_ptr as *const u8).is_null() {}
15+
if (fn_ptr as *const ()) == std::ptr::null() {}
16+
if (fn_ptr as *const ()) == (0 as *const ()) {}
17+
if (fn_ptr as *const ()) == ZPTR {}
18+
19+
// no lint
20+
if (fn_ptr as *const ()) == NOT_ZPTR {}
21+
}

tests/ui/fn_null_check.stderr

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
error: function pointer assumed to be nullable, even though it isn't
2+
--> $DIR/fn_null_check.rs:13:8
3+
|
4+
LL | if (fn_ptr as *mut ()).is_null() {}
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value
8+
= note: `-D clippy::fn-null-check` implied by `-D warnings`
9+
10+
error: function pointer assumed to be nullable, even though it isn't
11+
--> $DIR/fn_null_check.rs:14:8
12+
|
13+
LL | if (fn_ptr as *const u8).is_null() {}
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15+
|
16+
= help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value
17+
18+
error: function pointer assumed to be nullable, even though it isn't
19+
--> $DIR/fn_null_check.rs:15:8
20+
|
21+
LL | if (fn_ptr as *const ()) == std::ptr::null() {}
22+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
|
24+
= help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value
25+
26+
error: function pointer assumed to be nullable, even though it isn't
27+
--> $DIR/fn_null_check.rs:16:8
28+
|
29+
LL | if (fn_ptr as *const ()) == (0 as *const ()) {}
30+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31+
|
32+
= help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value
33+
34+
error: function pointer assumed to be nullable, even though it isn't
35+
--> $DIR/fn_null_check.rs:17:8
36+
|
37+
LL | if (fn_ptr as *const ()) == ZPTR {}
38+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39+
|
40+
= help: try wrapping your function pointer type in `Option<T>` instead, and using `is_none` to check for null pointer value
41+
42+
error: aborting due to 5 previous errors
43+

tests/ui/transmute_null_to_fn.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#![allow(dead_code)]
2+
#![warn(clippy::transmute_null_to_fn)]
3+
#![allow(clippy::zero_ptr)]
4+
5+
// Easy to lint because these only span one line.
6+
fn one_liners() {
7+
unsafe {
8+
let _: fn() = std::mem::transmute(0 as *const ());
9+
let _: fn() = std::mem::transmute(std::ptr::null::<()>());
10+
}
11+
}
12+
13+
pub const ZPTR: *const usize = 0 as *const _;
14+
pub const NOT_ZPTR: *const usize = 1 as *const _;
15+
16+
fn transmute_const() {
17+
unsafe {
18+
// Should raise a lint.
19+
let _: fn() = std::mem::transmute(ZPTR);
20+
// Should NOT raise a lint.
21+
let _: fn() = std::mem::transmute(NOT_ZPTR);
22+
}
23+
}
24+
25+
fn main() {
26+
one_liners();
27+
transmute_const();
28+
}

0 commit comments

Comments
 (0)