-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Implement non_send_field_in_send_ty
lint
#7709
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
Merged
Merged
Changes from 11 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e4c3000
Initial implementation
Qwaz d7a9ec2
Fix attribute handling
Qwaz 6458630
typo
Qwaz a81a5ad
Update documentation
Qwaz ee74574
Emit one report for all fields in the same ADT
Qwaz d413e15
Look into tuple, array, ADT args in raw pointer heuristic
Qwaz 427a09b
Add configuration for raw pointer heuristic
Qwaz 08f0aec
Minor changes from PR feedback
Qwaz 4f01656
Add ui-test for enable-raw-pointer-heuristic-for-send config
Qwaz dfed2e3
Do not use full type path in help message
Qwaz ef8df9d
Forgot to bless ui-toml test
Qwaz fb0353b
Update documentation and name for non_send_fields_in_send_ty lint
Qwaz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. | ||
/// | ||
/// ### 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(_)) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
enable-raw-pointer-heuristic-for-send = false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.