New lint: dangling_pointers_from_temporaries
This commit is contained in:
parent
81d6652e74
commit
c69894eaec
33 changed files with 1093 additions and 128 deletions
223
compiler/rustc_lint/src/dangling.rs
Normal file
223
compiler/rustc_lint/src/dangling.rs
Normal file
|
@ -0,0 +1,223 @@
|
|||
use rustc_ast::visit::{visit_opt, walk_list};
|
||||
use rustc_hir::def_id::LocalDefId;
|
||||
use rustc_hir::intravisit::{FnKind, Visitor, walk_expr};
|
||||
use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, LangItem};
|
||||
use rustc_middle::ty::{Ty, TyCtxt};
|
||||
use rustc_session::{declare_lint, impl_lint_pass};
|
||||
use rustc_span::Span;
|
||||
use rustc_span::symbol::sym;
|
||||
|
||||
use crate::lints::DanglingPointersFromTemporaries;
|
||||
use crate::{LateContext, LateLintPass};
|
||||
|
||||
declare_lint! {
|
||||
/// The `dangling_pointers_from_temporaries` lint detects getting a pointer to data
|
||||
/// of a temporary that will immediately get dropped.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![allow(unused)]
|
||||
/// # unsafe fn use_data(ptr: *const u8) { }
|
||||
/// fn gather_and_use(bytes: impl Iterator<Item = u8>) {
|
||||
/// let x: *const u8 = bytes.collect::<Vec<u8>>().as_ptr();
|
||||
/// unsafe { use_data(x) }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// {{produces}}
|
||||
///
|
||||
/// ### Explanation
|
||||
///
|
||||
/// Getting a pointer from a temporary value will not prolong its lifetime,
|
||||
/// which means that the value can be dropped and the allocation freed
|
||||
/// while the pointer still exists, making the pointer dangling.
|
||||
/// This is not an error (as far as the type system is concerned)
|
||||
/// but probably is not what the user intended either.
|
||||
///
|
||||
/// If you need stronger guarantees, consider using references instead,
|
||||
/// as they are statically verified by the borrow-checker to never dangle.
|
||||
pub DANGLING_POINTERS_FROM_TEMPORARIES,
|
||||
Warn,
|
||||
"detects getting a pointer from a temporary"
|
||||
}
|
||||
|
||||
/// FIXME: false negatives (i.e. the lint is not emitted when it should be)
|
||||
/// 1. Method calls that are not checked for:
|
||||
/// - [`temporary_unsafe_cell.get()`][`core::cell::UnsafeCell::get()`]
|
||||
/// - [`temporary_sync_unsafe_cell.get()`][`core::cell::SyncUnsafeCell::get()`]
|
||||
/// 2. Ways to get a temporary that are not recognized:
|
||||
/// - `owning_temporary.field`
|
||||
/// - `owning_temporary[index]`
|
||||
/// 3. No checks for ref-to-ptr conversions:
|
||||
/// - `&raw [mut] temporary`
|
||||
/// - `&temporary as *(const|mut) _`
|
||||
/// - `ptr::from_ref(&temporary)` and friends
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct DanglingPointers;
|
||||
|
||||
impl_lint_pass!(DanglingPointers => [DANGLING_POINTERS_FROM_TEMPORARIES]);
|
||||
|
||||
// This skips over const blocks, but they cannot use or return a dangling pointer anyways.
|
||||
impl<'tcx> LateLintPass<'tcx> for DanglingPointers {
|
||||
fn check_fn(
|
||||
&mut self,
|
||||
cx: &LateContext<'tcx>,
|
||||
_: FnKind<'tcx>,
|
||||
_: &'tcx FnDecl<'tcx>,
|
||||
body: &'tcx Body<'tcx>,
|
||||
_: Span,
|
||||
_: LocalDefId,
|
||||
) {
|
||||
DanglingPointerSearcher { cx, inside_call_args: false }.visit_body(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// This produces a dangling pointer:
|
||||
/// ```ignore (example)
|
||||
/// let ptr = CString::new("hello").unwrap().as_ptr();
|
||||
/// foo(ptr)
|
||||
/// ```
|
||||
///
|
||||
/// But this does not:
|
||||
/// ```ignore (example)
|
||||
/// foo(CString::new("hello").unwrap().as_ptr())
|
||||
/// ```
|
||||
///
|
||||
/// But this does:
|
||||
/// ```ignore (example)
|
||||
/// foo({ let ptr = CString::new("hello").unwrap().as_ptr(); ptr })
|
||||
/// ```
|
||||
///
|
||||
/// So we have to keep track of when we are inside of a function/method call argument.
|
||||
struct DanglingPointerSearcher<'lcx, 'tcx> {
|
||||
cx: &'lcx LateContext<'tcx>,
|
||||
/// Keeps track of whether we are inside of function/method call arguments,
|
||||
/// where this lint should not be emitted.
|
||||
///
|
||||
/// See [the main doc][`Self`] for examples.
|
||||
inside_call_args: bool,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for DanglingPointerSearcher<'_, '_> {
|
||||
fn visit_expr(&mut self, expr: &Expr<'_>) -> Self::Result {
|
||||
if !self.inside_call_args {
|
||||
lint_expr(self.cx, expr)
|
||||
}
|
||||
match expr.kind {
|
||||
ExprKind::Call(lhs, args) | ExprKind::MethodCall(_, lhs, args, _) => {
|
||||
self.visit_expr(lhs);
|
||||
self.with_inside_call_args(true, |this| walk_list!(this, visit_expr, args))
|
||||
}
|
||||
ExprKind::Block(&Block { stmts, expr, .. }, _) => {
|
||||
self.with_inside_call_args(false, |this| walk_list!(this, visit_stmt, stmts));
|
||||
visit_opt!(self, visit_expr, expr)
|
||||
}
|
||||
_ => walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DanglingPointerSearcher<'_, '_> {
|
||||
fn with_inside_call_args<R>(
|
||||
&mut self,
|
||||
inside_call_args: bool,
|
||||
callback: impl FnOnce(&mut Self) -> R,
|
||||
) -> R {
|
||||
let old = core::mem::replace(&mut self.inside_call_args, inside_call_args);
|
||||
let result = callback(self);
|
||||
self.inside_call_args = old;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn lint_expr(cx: &LateContext<'_>, expr: &Expr<'_>) {
|
||||
if let ExprKind::MethodCall(method, receiver, _args, _span) = expr.kind
|
||||
&& matches!(method.ident.name, sym::as_ptr | sym::as_mut_ptr)
|
||||
&& is_temporary_rvalue(receiver)
|
||||
&& let ty = cx.typeck_results().expr_ty(receiver)
|
||||
&& is_interesting(cx.tcx, ty)
|
||||
{
|
||||
// FIXME: use `emit_node_lint` when `#[primary_span]` is added.
|
||||
cx.tcx.emit_node_span_lint(
|
||||
DANGLING_POINTERS_FROM_TEMPORARIES,
|
||||
expr.hir_id,
|
||||
method.ident.span,
|
||||
DanglingPointersFromTemporaries {
|
||||
callee: method.ident.name,
|
||||
ty,
|
||||
ptr_span: method.ident.span,
|
||||
temporary_span: receiver.span,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_temporary_rvalue(expr: &Expr<'_>) -> bool {
|
||||
match expr.kind {
|
||||
// Const is not temporary.
|
||||
ExprKind::ConstBlock(..) | ExprKind::Repeat(..) | ExprKind::Lit(..) => false,
|
||||
|
||||
// This is literally lvalue.
|
||||
ExprKind::Path(..) => false,
|
||||
|
||||
// Calls return rvalues.
|
||||
ExprKind::Call(..) | ExprKind::MethodCall(..) | ExprKind::Binary(..) => true,
|
||||
|
||||
// Inner blocks are rvalues.
|
||||
ExprKind::If(..) | ExprKind::Loop(..) | ExprKind::Match(..) | ExprKind::Block(..) => true,
|
||||
|
||||
// FIXME: these should probably recurse and typecheck along the way.
|
||||
// Some false negatives are possible for now.
|
||||
ExprKind::Index(..) | ExprKind::Field(..) | ExprKind::Unary(..) => false,
|
||||
|
||||
ExprKind::Struct(..) => true,
|
||||
|
||||
// FIXME: this has false negatives, but I do not want to deal with 'static/const promotion just yet.
|
||||
ExprKind::Array(..) => false,
|
||||
|
||||
// These typecheck to `!`
|
||||
ExprKind::Break(..) | ExprKind::Continue(..) | ExprKind::Ret(..) | ExprKind::Become(..) => {
|
||||
false
|
||||
}
|
||||
|
||||
// These typecheck to `()`
|
||||
ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::Yield(..) => false,
|
||||
|
||||
// Compiler-magic macros
|
||||
ExprKind::AddrOf(..) | ExprKind::OffsetOf(..) | ExprKind::InlineAsm(..) => false,
|
||||
|
||||
// We are not interested in these
|
||||
ExprKind::Cast(..)
|
||||
| ExprKind::Closure(..)
|
||||
| ExprKind::Tup(..)
|
||||
| ExprKind::DropTemps(..)
|
||||
| ExprKind::Let(..) => false,
|
||||
|
||||
// Not applicable
|
||||
ExprKind::Type(..) | ExprKind::Err(..) => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Array, Vec, String, CString, MaybeUninit, Cell, Box<[_]>, Box<str>, Box<CStr>,
|
||||
// or any of the above in arbitrary many nested Box'es.
|
||||
fn is_interesting(tcx: TyCtxt<'_>, ty: Ty<'_>) -> bool {
|
||||
if ty.is_array() {
|
||||
true
|
||||
} else if let Some(inner) = ty.boxed_ty() {
|
||||
inner.is_slice()
|
||||
|| inner.is_str()
|
||||
|| inner.ty_adt_def().is_some_and(|def| tcx.is_lang_item(def.did(), LangItem::CStr))
|
||||
|| is_interesting(tcx, inner)
|
||||
} else if let Some(def) = ty.ty_adt_def() {
|
||||
for lang_item in [LangItem::String, LangItem::MaybeUninit] {
|
||||
if tcx.is_lang_item(def.did(), lang_item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
tcx.get_diagnostic_name(def.did())
|
||||
.is_some_and(|name| matches!(name, sym::cstring_type | sym::Vec | sym::Cell))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
|
@ -46,6 +46,7 @@ mod async_closures;
|
|||
mod async_fn_in_trait;
|
||||
pub mod builtin;
|
||||
mod context;
|
||||
mod dangling;
|
||||
mod deref_into_dyn_supertrait;
|
||||
mod drop_forget_useless;
|
||||
mod early;
|
||||
|
@ -65,7 +66,6 @@ mod levels;
|
|||
mod lints;
|
||||
mod macro_expr_fragment_specifier_2024_migration;
|
||||
mod map_unit_fn;
|
||||
mod methods;
|
||||
mod multiple_supertrait_upcastable;
|
||||
mod non_ascii_idents;
|
||||
mod non_fmt_panic;
|
||||
|
@ -91,6 +91,7 @@ mod unused;
|
|||
use async_closures::AsyncClosureUsage;
|
||||
use async_fn_in_trait::AsyncFnInTrait;
|
||||
use builtin::*;
|
||||
use dangling::*;
|
||||
use deref_into_dyn_supertrait::*;
|
||||
use drop_forget_useless::*;
|
||||
use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums;
|
||||
|
@ -103,7 +104,6 @@ use invalid_from_utf8::*;
|
|||
use let_underscore::*;
|
||||
use macro_expr_fragment_specifier_2024_migration::*;
|
||||
use map_unit_fn::*;
|
||||
use methods::*;
|
||||
use multiple_supertrait_upcastable::*;
|
||||
use non_ascii_idents::*;
|
||||
use non_fmt_panic::NonPanicFmt;
|
||||
|
@ -231,7 +231,7 @@ late_lint_methods!(
|
|||
UngatedAsyncFnTrackCaller: UngatedAsyncFnTrackCaller,
|
||||
ShadowedIntoIter: ShadowedIntoIter,
|
||||
DropTraitConstraints: DropTraitConstraints,
|
||||
TemporaryCStringAsPtr: TemporaryCStringAsPtr,
|
||||
DanglingPointers: DanglingPointers,
|
||||
NonPanicFmt: NonPanicFmt,
|
||||
NoopMethodCall: NoopMethodCall,
|
||||
EnumIntrinsicsNonEnums: EnumIntrinsicsNonEnums,
|
||||
|
@ -356,6 +356,7 @@ fn register_builtins(store: &mut LintStore) {
|
|||
store.register_renamed("non_fmt_panic", "non_fmt_panics");
|
||||
store.register_renamed("unused_tuple_struct_fields", "dead_code");
|
||||
store.register_renamed("static_mut_ref", "static_mut_refs");
|
||||
store.register_renamed("temporary_cstring_as_ptr", "dangling_pointers_from_temporaries");
|
||||
|
||||
// These were moved to tool lints, but rustc still sees them when compiling normally, before
|
||||
// tool lints are registered, so `check_tool_name_for_backwards_compat` doesn't work. Use
|
||||
|
|
|
@ -1137,16 +1137,19 @@ pub(crate) struct IgnoredUnlessCrateSpecified<'a> {
|
|||
pub name: Symbol,
|
||||
}
|
||||
|
||||
// methods.rs
|
||||
// dangling.rs
|
||||
#[derive(LintDiagnostic)]
|
||||
#[diag(lint_cstring_ptr)]
|
||||
#[diag(lint_dangling_pointers_from_temporaries)]
|
||||
#[note]
|
||||
#[help]
|
||||
pub(crate) struct CStringPtr {
|
||||
#[label(lint_as_ptr_label)]
|
||||
pub as_ptr: Span,
|
||||
#[label(lint_unwrap_label)]
|
||||
pub unwrap: Span,
|
||||
// FIXME: put #[primary_span] on `ptr_span` once it does not cause conflicts
|
||||
pub(crate) struct DanglingPointersFromTemporaries<'tcx> {
|
||||
pub callee: Symbol,
|
||||
pub ty: Ty<'tcx>,
|
||||
#[label(lint_label_ptr)]
|
||||
pub ptr_span: Span,
|
||||
#[label(lint_label_temporary)]
|
||||
pub temporary_span: Span,
|
||||
}
|
||||
|
||||
// multiple_supertrait_upcastable.rs
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
use rustc_hir::{Expr, ExprKind};
|
||||
use rustc_middle::ty;
|
||||
use rustc_session::{declare_lint, declare_lint_pass};
|
||||
use rustc_span::Span;
|
||||
use rustc_span::symbol::sym;
|
||||
|
||||
use crate::lints::CStringPtr;
|
||||
use crate::{LateContext, LateLintPass, LintContext};
|
||||
|
||||
declare_lint! {
|
||||
/// The `temporary_cstring_as_ptr` lint detects getting the inner pointer of
|
||||
/// a temporary `CString`.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![allow(unused)]
|
||||
/// # use std::ffi::CString;
|
||||
/// let c_str = CString::new("foo").unwrap().as_ptr();
|
||||
/// ```
|
||||
///
|
||||
/// {{produces}}
|
||||
///
|
||||
/// ### Explanation
|
||||
///
|
||||
/// The inner pointer of a `CString` lives only as long as the `CString` it
|
||||
/// points to. Getting the inner pointer of a *temporary* `CString` allows the `CString`
|
||||
/// to be dropped at the end of the statement, as it is not being referenced as far as the
|
||||
/// typesystem is concerned. This means outside of the statement the pointer will point to
|
||||
/// freed memory, which causes undefined behavior if the pointer is later dereferenced.
|
||||
pub TEMPORARY_CSTRING_AS_PTR,
|
||||
Warn,
|
||||
"detects getting the inner pointer of a temporary `CString`"
|
||||
}
|
||||
|
||||
declare_lint_pass!(TemporaryCStringAsPtr => [TEMPORARY_CSTRING_AS_PTR]);
|
||||
|
||||
impl<'tcx> LateLintPass<'tcx> for TemporaryCStringAsPtr {
|
||||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
|
||||
if let ExprKind::MethodCall(as_ptr_path, as_ptr_receiver, ..) = expr.kind
|
||||
&& as_ptr_path.ident.name == sym::as_ptr
|
||||
&& let ExprKind::MethodCall(unwrap_path, unwrap_receiver, ..) = as_ptr_receiver.kind
|
||||
&& (unwrap_path.ident.name == sym::unwrap || unwrap_path.ident.name == sym::expect)
|
||||
{
|
||||
lint_cstring_as_ptr(cx, as_ptr_path.ident.span, unwrap_receiver, as_ptr_receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lint_cstring_as_ptr(
|
||||
cx: &LateContext<'_>,
|
||||
as_ptr_span: Span,
|
||||
source: &rustc_hir::Expr<'_>,
|
||||
unwrap: &rustc_hir::Expr<'_>,
|
||||
) {
|
||||
let source_type = cx.typeck_results().expr_ty(source);
|
||||
if let ty::Adt(def, args) = source_type.kind() {
|
||||
if cx.tcx.is_diagnostic_item(sym::Result, def.did()) {
|
||||
if let ty::Adt(adt, _) = args.type_at(0).kind() {
|
||||
if cx.tcx.is_diagnostic_item(sym::cstring_type, adt.did()) {
|
||||
cx.emit_span_lint(TEMPORARY_CSTRING_AS_PTR, as_ptr_span, CStringPtr {
|
||||
as_ptr: as_ptr_span,
|
||||
unwrap: unwrap.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue