Skip to content
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

Implement non_send_field_in_send_ty lint #7709

Merged
merged 12 commits into from
Oct 3, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2898,6 +2898,7 @@ Released 2018-09-13
[`no_effect`]: https://rust-lang.github.io/rust-clippy/master/index.html#no_effect
[`non_ascii_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_ascii_literal
[`non_octal_unix_permissions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_octal_unix_permissions
[`non_send_field_in_send_ty`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_send_field_in_send_ty
[`nonminimal_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonminimal_bool
[`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options
[`nonstandard_macro_braces`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonstandard_macro_braces
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ mod no_effect;
mod non_copy_const;
mod non_expressive_names;
mod non_octal_unix_permissions;
mod non_send_field_in_send_ty;
mod nonstandard_macro_braces;
mod open_options;
mod option_env_unwrap;
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ store.register_lints(&[
non_expressive_names::MANY_SINGLE_CHAR_NAMES,
non_expressive_names::SIMILAR_NAMES,
non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS,
non_send_field_in_send_ty::NON_SEND_FIELD_IN_SEND_TY,
nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES,
open_options::NONSENSICAL_OPEN_OPTIONS,
option_env_unwrap::OPTION_ENV_UNWRAP,
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ store.register_group(true, "clippy::nursery", Some("clippy_nursery"), vec![
LintId::of(missing_const_for_fn::MISSING_CONST_FOR_FN),
LintId::of(mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL),
LintId::of(mutex_atomic::MUTEX_INTEGER),
LintId::of(non_send_field_in_send_ty::NON_SEND_FIELD_IN_SEND_TY),
LintId::of(nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES),
LintId::of(option_if_let_else::OPTION_IF_LET_ELSE),
LintId::of(path_buf_push_overwrite::PATH_BUF_PUSH_OVERWRITE),
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 @@ -535,6 +535,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(move || Box::new(feature_name::FeatureName));
store.register_late_pass(move || Box::new(iter_not_returning_iterator::IterNotReturningIterator));
store.register_late_pass(move || Box::new(if_then_panic::IfThenPanic));
let enable_raw_pointer_heuristic_for_send = conf.enable_raw_pointer_heuristic_for_send;
store.register_late_pass(move || Box::new(non_send_field_in_send_ty::NonSendFieldInSendTy::new(enable_raw_pointer_heuristic_for_send)));
}

#[rustfmt::skip]
Expand Down
238 changes: 238 additions & 0 deletions clippy_lints/src/non_send_field_in_send_ty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::is_lint_allowed;
use clippy_utils::source::snippet;
use clippy_utils::ty::{implements_trait, is_copy};
use rustc_ast::ImplPolarity;
use rustc_hir::def_id::DefId;
use rustc_hir::{FieldDef, Item, ItemKind, Node};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, subst::GenericArgKind, Ty};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::sym;

declare_clippy_lint! {
/// ### What it does
/// Warns about a field in a `Send` struct that is neither `Send` nor `Copy`.
Qwaz marked this conversation as resolved.
Show resolved Hide resolved
///
/// ### Why is this bad?
/// Sending the struct to another thread will transfer the ownership to
/// the new thread by dropping in the current thread during the transfer.
/// This causes soundness issues for non-`Send` fields, as they are also
/// dropped and might not be set up to handle this.
///
/// See:
/// * [*The Rustonomicon* about *Send and Sync*](https://doc.rust-lang.org/nomicon/send-and-sync.html)
/// * [The documentation of `Send`](https://doc.rust-lang.org/std/marker/trait.Send.html)
///
Qwaz marked this conversation as resolved.
Show resolved Hide resolved
/// ### Known Problems
/// Data structures that contain raw pointers may cause false positives.
/// They are sometimes safe to be sent across threads but do not implement
/// the `Send` trait. This lint has a heuristic to filter out basic cases
/// such as `Vec<*const T>`, but it's not perfect. Feel free to create an
/// issue if you have a suggestion on how this heuristic can be improved.
///
/// ### Example
/// ```rust,ignore
/// struct ExampleStruct<T> {
/// rc_is_not_send: Rc<String>,
/// unbounded_generic_field: T,
/// }
///
/// // This impl is unsound because it allows sending `!Send` types through `ExampleStruct`
/// unsafe impl<T> Send for ExampleStruct<T> {}
/// ```
/// Use thread-safe types like [`std::sync::Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html)
/// or specify correct bounds on generic type parameters (`T: Send`).
pub NON_SEND_FIELD_IN_SEND_TY,
Qwaz marked this conversation as resolved.
Show resolved Hide resolved
nursery,
"there is field that does not implement `Send` in a `Send` struct"
}

#[derive(Copy, Clone)]
pub struct NonSendFieldInSendTy {
enable_raw_pointer_heuristic: bool,
}

impl NonSendFieldInSendTy {
pub fn new(enable_raw_pointer_heuristic: bool) -> Self {
Self {
enable_raw_pointer_heuristic,
}
}
}

impl_lint_pass!(NonSendFieldInSendTy => [NON_SEND_FIELD_IN_SEND_TY]);

impl<'tcx> LateLintPass<'tcx> for NonSendFieldInSendTy {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
let ty_allowed_in_send = if self.enable_raw_pointer_heuristic {
ty_allowed_with_raw_pointer_heuristic
} else {
ty_allowed_without_raw_pointer_heuristic
};

// Checks if we are in `Send` impl item.
// We start from `Send` impl instead of `check_field_def()` because
// single `AdtDef` may have multiple `Send` impls due to generic
// parameters, and the lint is much easier to implement in this way.
if_chain! {
if let Some(send_trait) = cx.tcx.get_diagnostic_item(sym::send_trait);
if let ItemKind::Impl(hir_impl) = &item.kind;
if let Some(trait_ref) = &hir_impl.of_trait;
if let Some(trait_id) = trait_ref.trait_def_id();
if send_trait == trait_id;
if let ImplPolarity::Positive = hir_impl.polarity;
if let Some(ty_trait_ref) = cx.tcx.impl_trait_ref(item.def_id);
if let self_ty = ty_trait_ref.self_ty();
if let ty::Adt(adt_def, impl_trait_substs) = self_ty.kind();
then {
let mut non_send_fields = Vec::new();

let hir_map = cx.tcx.hir();
for variant in &adt_def.variants {
for field in &variant.fields {
Qwaz marked this conversation as resolved.
Show resolved Hide resolved
if_chain! {
if let Some(field_hir_id) = field
.did
.as_local()
.map(|local_def_id| hir_map.local_def_id_to_hir_id(local_def_id));
if !is_lint_allowed(cx, NON_SEND_FIELD_IN_SEND_TY, field_hir_id);
if let field_ty = field.ty(cx.tcx, impl_trait_substs);
if !ty_allowed_in_send(cx, field_ty, send_trait);
if let Node::Field(field_def) = hir_map.get(field_hir_id);
then {
non_send_fields.push(NonSendField {
def: field_def,
ty: field_ty,
generic_params: collect_generic_params(cx, field_ty),
})
}
}
}
}

if !non_send_fields.is_empty() {
span_lint_and_then(
cx,
NON_SEND_FIELD_IN_SEND_TY,
item.span,
&format!(
"this implementation is unsound, as some fields in `{}` are `!Send`",
snippet(cx, hir_impl.self_ty.span, "Unknown")
),
|diag| {
for field in non_send_fields {
diag.span_note(
field.def.span,
&format!("the type of field `{}` is `!Send`", field.def.ident.name),
);

match field.generic_params.len() {
0 => diag.help("use a thread-safe type that implements `Send`"),
1 if is_ty_param(field.ty) => diag.help(&format!("add `{}: Send` bound in `Send` impl", field.ty)),
_ => diag.help(&format!(
"add bounds on type parameter{} `{}` that satisfy `{}: Send`",
if field.generic_params.len() > 1 { "s" } else { "" },
field.generic_params_string(),
snippet(cx, field.def.ty.span, "Unknown"),
)),
};
}
},
);
}
}
}
}
}

struct NonSendField<'tcx> {
def: &'tcx FieldDef<'tcx>,
ty: Ty<'tcx>,
generic_params: Vec<Ty<'tcx>>,
}

impl<'tcx> NonSendField<'tcx> {
fn generic_params_string(&self) -> String {
self.generic_params
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
}

/// Given a type, collect all of its generic parameters.
/// Example: `MyStruct<P, Box<Q, R>>` => `vec![P, Q, R]`
fn collect_generic_params<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Vec<Ty<'tcx>> {
ty.walk(cx.tcx)
.filter_map(|inner| match inner.unpack() {
GenericArgKind::Type(inner_ty) => Some(inner_ty),
_ => None,
})
.filter(|&inner_ty| is_ty_param(inner_ty))
.collect()
}

/// Be more strict when the heuristic is disabled
fn ty_allowed_without_raw_pointer_heuristic<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, send_trait: DefId) -> bool {
if implements_trait(cx, ty, send_trait, &[]) {
return true;
}

if is_copy(cx, ty) && !contains_raw_pointer(cx, ty) {
return true;
}

false
}

/// Heuristic to allow cases like `Vec<*const u8>`
fn ty_allowed_with_raw_pointer_heuristic<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, send_trait: DefId) -> bool {
if implements_trait(cx, ty, send_trait, &[]) || is_copy(cx, ty) {
return true;
}

// The type is known to be `!Send` and `!Copy`
match ty.kind() {
ty::Tuple(_) => ty
.tuple_fields()
.all(|ty| ty_allowed_with_raw_pointer_heuristic(cx, ty, send_trait)),
ty::Array(ty, _) | ty::Slice(ty) => ty_allowed_with_raw_pointer_heuristic(cx, ty, send_trait),
ty::Adt(_, substs) => {
if contains_raw_pointer(cx, ty) {
// descends only if ADT contains any raw pointers
substs.iter().all(|generic_arg| match generic_arg.unpack() {
GenericArgKind::Type(ty) => ty_allowed_with_raw_pointer_heuristic(cx, ty, send_trait),
// Lifetimes and const generics are not solid part of ADT and ignored
GenericArgKind::Lifetime(_) | GenericArgKind::Const(_) => true,
})
} else {
false
}
},
// Raw pointers are `!Send` but allowed by the heuristic
ty::RawPtr(_) => true,
_ => false,
}
}

/// Checks if the type contains any raw pointers in substs (including nested ones).
fn contains_raw_pointer<'tcx>(cx: &LateContext<'tcx>, target_ty: Ty<'tcx>) -> bool {
for ty_node in target_ty.walk(cx.tcx) {
if_chain! {
if let GenericArgKind::Type(inner_ty) = ty_node.unpack();
if let ty::RawPtr(_) = inner_ty.kind();
then {
return true;
}
}
}

false
}

/// Returns `true` if the type is a type parameter such as `T`.
fn is_ty_param(target_ty: Ty<'_>) -> bool {
matches!(target_ty.kind(), ty::Param(_))
}
4 changes: 4 additions & 0 deletions clippy_lints/src/utils/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ define_Conf! {
///
/// The list of unicode scripts allowed to be used in the scope.
(allowed_scripts: Vec<String> = vec!["Latin".to_string()]),
/// Lint: NON_SEND_FIELD_IN_SEND_TY.
///
/// Whether to apply the raw pointer heuristic in `non_send_field_in_send_ty` lint.
Qwaz marked this conversation as resolved.
Show resolved Hide resolved
(enable_raw_pointer_heuristic_for_send: bool = true),
}

/// Search for the configuration file.
Expand Down
1 change: 1 addition & 0 deletions tests/ui-toml/strict_non_send_field_in_send_ty/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enable-raw-pointer-heuristic-for-send = false
43 changes: 43 additions & 0 deletions tests/ui-toml/strict_non_send_field_in_send_ty/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#![warn(clippy::non_send_field_in_send_ty)]
#![feature(extern_types)]

use std::rc::Rc;

// Basic tests should not be affected
pub struct NoGeneric {
rc_is_not_send: Rc<String>,
}

unsafe impl Send for NoGeneric {}

pub struct MultiField<T> {
field1: T,
field2: T,
field3: T,
}

unsafe impl<T> Send for MultiField<T> {}

pub enum MyOption<T> {
MySome(T),
MyNone,
}

unsafe impl<T> Send for MyOption<T> {}

// All fields are disallowed when raw pointer heuristic is off
extern "C" {
type NonSend;
}

pub struct HeuristicTest {
field1: Vec<*const NonSend>,
field2: [*const NonSend; 3],
field3: (*const NonSend, *const NonSend, *const NonSend),
field4: (*const NonSend, Rc<u8>),
field5: Vec<Vec<*const NonSend>>,
}

unsafe impl Send for HeuristicTest {}

fn main() {}
Loading