Skip to content

Add new lint macros_hiding_unsafe_code #7469

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

Closed
Closed
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 @@ -2613,6 +2613,7 @@ Released 2018-09-13
[`logic_bug`]: https://rust-lang.github.io/rust-clippy/master/index.html#logic_bug
[`lossy_float_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#lossy_float_literal
[`macro_use_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#macro_use_imports
[`macros_hiding_unsafe_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#macros_hiding_unsafe_code
[`main_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#main_recursion
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
Expand Down
4 changes: 4 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ mod lifetimes;
mod literal_representation;
mod loops;
mod macro_use;
mod macros_hiding_unsafe_code;
mod main_recursion;
mod manual_async_fn;
mod manual_map;
Expand Down Expand Up @@ -707,6 +708,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
loops::WHILE_LET_LOOP,
loops::WHILE_LET_ON_ITERATOR,
macro_use::MACRO_USE_IMPORTS,
macros_hiding_unsafe_code::MACROS_HIDING_UNSAFE_CODE,
main_recursion::MAIN_RECURSION,
manual_async_fn::MANUAL_ASYNC_FN,
manual_map::MANUAL_MAP,
Expand Down Expand Up @@ -1018,6 +1020,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(integer_division::INTEGER_DIVISION),
LintId::of(let_underscore::LET_UNDERSCORE_MUST_USE),
LintId::of(literal_representation::DECIMAL_LITERAL_REPRESENTATION),
LintId::of(macros_hiding_unsafe_code::MACROS_HIDING_UNSAFE_CODE),
LintId::of(map_err_ignore::MAP_ERR_IGNORE),
LintId::of(matches::REST_PAT_IN_FULLY_BOUND_STRUCTS),
LintId::of(matches::WILDCARD_ENUM_MATCH_ARM),
Expand Down Expand Up @@ -2101,6 +2104,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
let scripts = conf.allowed_scripts.clone();
store.register_early_pass(move || box disallowed_script_idents::DisallowedScriptIdents::new(&scripts));
store.register_late_pass(|| box strlen_on_c_strings::StrlenOnCStrings);
store.register_late_pass(|| box macros_hiding_unsafe_code::MacrosHidingUnsafeCode::default());
}

#[rustfmt::skip]
Expand Down
140 changes: 140 additions & 0 deletions clippy_lints/src/macros_hiding_unsafe_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use clippy_utils::{diagnostics::span_lint_and_sugg, in_macro, is_lint_allowed, source::snippet_with_applicability};
use rustc_errors::Applicability;
use rustc_hir::{intravisit::FnKind, Block, BlockCheckMode, Body, FnDecl, FnSig, HirId, UnsafeSource, Unsafety};
use rustc_lint::{
builtin::{UNSAFE_OP_IN_UNSAFE_FN, UNUSED_UNSAFE},
LateContext, LateLintPass, LintContext,
};
use rustc_middle::lint::in_external_macro;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::Span;

declare_clippy_lint! {
/// **What it does:** Checks for macro calls inside an `unsafe` function which expansion
/// contains an `unsafe` block, while the macro call is not wrapped in an `unsafe` block
/// itself. This lint only triggers in functions where the `unsafe_op_in_unsafe_fn` lint is
/// enabled.
///
/// **Why is this bad?** This hides an unsafe operation inside a macro call. This is against
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that being against the intention of lint is a strong motivation since it's the nature of the lint to catch what it's supposed to catch. Like, chicken and egg problem.

I'd emphasize that the macro containing an unsafe can hide the fact that it uses unsafe, and thus makes it easier to use in the context where required invariants are not held.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll word this differently.

/// the intention of the `unsafe_op_in_unsafe_fn` lint, which is meant to make unsafe code more
/// visible by requiring `unsafe` blocks also in `unsafe` functions.
///
/// **Known problems:**
/// - In some cases the intention of the macro is to actually hide the unsafety, because the
/// macro itself ensures the safety of the `unsafe` operation.
/// - For local macros, either an `#[allow(unused_unsafe)]` has to be added to the new unsafe
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, suggesting introducing an unused unsafe block is not a good thing to do; and suggesting allow(unused_unsafe) is worse.

Imagine the following chain of actions:

  1. Someone introduces a macro with an unsafe code.
  2. In the use places it gets wrapped with #[allow(unused_unsafe)] unsafe { ... }.
  3. Unsafe code is removed from macro.

There is a good chance that this unused_unsafe will remain there unnoticed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also think this isn't nice. But I don't really see how else to handle this, without rendering the lint completely useless.

Copy link
Member Author

@flip1995 flip1995 Jul 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When implementing the lint and noticing, that this is necessary, I also thought about whether we should allow lints that contradict rustc lints at all. We have lints contradicting other Clippy lints in the restriction group. But we don't have lints that contradict rustc lints (yet).

/// block or the unsafe block inside the macro has to be removed.
///
/// **Example:**
///
/// ```rust
/// macro_rules! unsafe_macro {
/// ($e:expr) => {
/// unsafe { $e };
/// };
/// }
///
/// #[warn(unsafe_op_in_unsafe_fn)]
/// unsafe fn foo(x: *const usize) {
/// unsafe_macro!(std::ptr::read(x));
/// }
/// ```
/// Use instead:
/// ```rust
/// # macro_rules! unsafe_macro {
/// # ($e:expr) => {
/// # unsafe { $e };
/// # };
/// # }
/// #[warn(unsafe_op_in_unsafe_fn)]
/// unsafe fn foo(x: *const usize) {
/// #[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)) };
/// }
/// ```
pub MACROS_HIDING_UNSAFE_CODE,
restriction,
"macros hiding unsafe code, while `unsafe_op_in_unsafe_fn` is enabled"
}

#[derive(Clone, Copy, Default)]
pub struct MacrosHidingUnsafeCode {
in_unsafe_fn: bool,
in_unsafe_block: usize,
}

impl_lint_pass!(MacrosHidingUnsafeCode => [MACROS_HIDING_UNSAFE_CODE]);

impl LateLintPass<'_> for MacrosHidingUnsafeCode {
fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules {
if in_macro(block.span) {
if self.in_unsafe_fn
&& self.in_unsafe_block == 0
&& !is_lint_allowed(cx, UNSAFE_OP_IN_UNSAFE_FN, block.hir_id)
{
let macro_call_span = block.span.source_callsite();
let unused_unsafe_sugg = if !in_external_macro(cx.sess(), block.span)
&& !is_lint_allowed(cx, UNUSED_UNSAFE, block.hir_id)
{
"#[allow(unused_unsafe)] "
} else {
""
};
let mut applicability = Applicability::MaybeIncorrect;
span_lint_and_sugg(
cx,
MACROS_HIDING_UNSAFE_CODE,
macro_call_span,
"this macro call hides unsafe code",
"wrap it in an `unsafe` block",
format!(
"{}unsafe {{ {} }}",
unused_unsafe_sugg,
snippet_with_applicability(cx, macro_call_span, "...", &mut applicability),
),
applicability,
);
}
} else {
self.in_unsafe_block = self.in_unsafe_block.saturating_add(1);
}
}
}

fn check_block_post(&mut self, _: &LateContext<'_>, block: &Block<'_>) {
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules {
if !in_macro(block.span) {
self.in_unsafe_block = self.in_unsafe_block.saturating_sub(1);
}
}
}

fn check_fn(&mut self, _: &LateContext<'_>, fn_kind: FnKind<'_>, _: &FnDecl<'_>, _: &Body<'_>, _: Span, _: HirId) {
if is_unsafe_fn(fn_kind) {
self.in_unsafe_fn = true;
}
}

fn check_fn_post(
&mut self,
_: &LateContext<'_>,
fn_kind: FnKind<'_>,
_: &FnDecl<'_>,
_: &Body<'_>,
_: Span,
_: HirId,
) {
if is_unsafe_fn(fn_kind) {
self.in_unsafe_fn = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be adjusted for nested functions.

unsafe fn f() {
    fn g() {}
    unsafe_macro!();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. Your example shouldn't break this code, but

unsafe fn f() {
    unsafe fn g() {}
    unsafe_macro!();
}

will.

}
}

fn is_unsafe_fn(fn_kind: FnKind<'_>) -> bool {
match fn_kind {
FnKind::ItemFn(_, _, header, _) | FnKind::Method(_, &FnSig { header, .. }, _) => {
matches!(header.unsafety, Unsafety::Unsafe)
},
FnKind::Closure => false,
}
}
7 changes: 7 additions & 0 deletions tests/ui/auxiliary/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,10 @@ macro_rules! default_numeric_fallback {
let x = 22;
};
}

#[macro_export]
macro_rules! unsafe_external_macro {
($e:expr) => {
unsafe { $e }
};
}
69 changes: 69 additions & 0 deletions tests/ui/macros_hiding_unsafe_code.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// run-rustfix
// aux-build: macro_rules.rs

#![warn(clippy::macros_hiding_unsafe_code)]
// compiletest allows unused code by default. But we want to explicitely test
// for this lint here.
#![warn(unused_unsafe)]

extern crate macro_rules;

use macro_rules::unsafe_external_macro;

macro_rules! unsafe_macro {
($e:expr) => {
unsafe { $e }
};
(NESTED $e:expr) => {
unsafe { unsafe_function($e) }
};
}

unsafe fn unsafe_function(x: *const usize) {
let _ = std::ptr::read(x);
}

mod no_unsafe_op_in_unsafe_fn {
use super::*;

#[allow(unused_unsafe)]
pub unsafe fn ok(x: *const usize) {
unsafe_macro!(std::ptr::read(x));
unsafe_external_macro!(std::ptr::read(x));
}
}

mod unsafe_op_in_unsafe_fn {
#![warn(unsafe_op_in_unsafe_fn)]

use super::*;

pub unsafe fn bad(x: *const usize) {
#[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)); }
#[allow(unused_unsafe)] unsafe { unsafe_macro!(NESTED std::ptr::read(&x)); }
unsafe { unsafe_external_macro!(std::ptr::read(x)); }
}

pub unsafe fn ok(x: *const usize) {
#[allow(unused_unsafe)]
unsafe {
unsafe_macro!(std::ptr::read(x));
unsafe_external_macro!(std::ptr::read(x));
}
}

#[allow(unused_unsafe)]
pub unsafe fn unused_unsafe_disabled(x: *const usize) {
unsafe { unsafe_macro!(std::ptr::read(x)); }
unsafe { unsafe_external_macro!(std::ptr::read(x)); }
}
}

fn main() {
unsafe {
no_unsafe_op_in_unsafe_fn::ok(std::ptr::null());
unsafe_op_in_unsafe_fn::bad(std::ptr::null());
unsafe_op_in_unsafe_fn::ok(std::ptr::null());
unsafe_op_in_unsafe_fn::unused_unsafe_disabled(std::ptr::null());
}
}
69 changes: 69 additions & 0 deletions tests/ui/macros_hiding_unsafe_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// run-rustfix
// aux-build: macro_rules.rs

#![warn(clippy::macros_hiding_unsafe_code)]
// compiletest allows unused code by default. But we want to explicitely test
// for this lint here.
#![warn(unused_unsafe)]

extern crate macro_rules;

use macro_rules::unsafe_external_macro;

macro_rules! unsafe_macro {
($e:expr) => {
unsafe { $e }
};
(NESTED $e:expr) => {
unsafe { unsafe_function($e) }
};
}

unsafe fn unsafe_function(x: *const usize) {
let _ = std::ptr::read(x);
}

mod no_unsafe_op_in_unsafe_fn {
use super::*;

#[allow(unused_unsafe)]
pub unsafe fn ok(x: *const usize) {
unsafe_macro!(std::ptr::read(x));
unsafe_external_macro!(std::ptr::read(x));
}
}

mod unsafe_op_in_unsafe_fn {
#![warn(unsafe_op_in_unsafe_fn)]

use super::*;

pub unsafe fn bad(x: *const usize) {
unsafe_macro!(std::ptr::read(x));
unsafe_macro!(NESTED std::ptr::read(&x));
unsafe_external_macro!(std::ptr::read(x));
}

pub unsafe fn ok(x: *const usize) {
#[allow(unused_unsafe)]
unsafe {
unsafe_macro!(std::ptr::read(x));
unsafe_external_macro!(std::ptr::read(x));
}
}

#[allow(unused_unsafe)]
pub unsafe fn unused_unsafe_disabled(x: *const usize) {
unsafe_macro!(std::ptr::read(x));
unsafe_external_macro!(std::ptr::read(x));
}
}

fn main() {
unsafe {
no_unsafe_op_in_unsafe_fn::ok(std::ptr::null());
unsafe_op_in_unsafe_fn::bad(std::ptr::null());
unsafe_op_in_unsafe_fn::ok(std::ptr::null());
unsafe_op_in_unsafe_fn::unused_unsafe_disabled(std::ptr::null());
}
}
34 changes: 34 additions & 0 deletions tests/ui/macros_hiding_unsafe_code.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
error: this macro call hides unsafe code
--> $DIR/macros_hiding_unsafe_code.rs:42:9
|
LL | unsafe_macro!(std::ptr::read(x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `#[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)); }`
|
= note: `-D clippy::macros-hiding-unsafe-code` implied by `-D warnings`

error: this macro call hides unsafe code
--> $DIR/macros_hiding_unsafe_code.rs:43:9
|
LL | unsafe_macro!(NESTED std::ptr::read(&x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `#[allow(unused_unsafe)] unsafe { unsafe_macro!(NESTED std::ptr::read(&x)); }`

error: this macro call hides unsafe code
--> $DIR/macros_hiding_unsafe_code.rs:44:9
|
LL | unsafe_external_macro!(std::ptr::read(x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_external_macro!(std::ptr::read(x)); }`

error: this macro call hides unsafe code
--> $DIR/macros_hiding_unsafe_code.rs:57:9
|
LL | unsafe_macro!(std::ptr::read(x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_macro!(std::ptr::read(x)); }`

error: this macro call hides unsafe code
--> $DIR/macros_hiding_unsafe_code.rs:58:9
|
LL | unsafe_external_macro!(std::ptr::read(x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_external_macro!(std::ptr::read(x)); }`

error: aborting due to 5 previous errors