about summary refs log tree commit diff
path: root/compiler/rustc_lint/src
diff options
context:
space:
mode:
authorPavel Grigorenko <GrigorenkoPV@ya.ru>2024-10-16 22:19:56 +0300
committerPavel Grigorenko <GrigorenkoPV@ya.ru>2024-10-28 14:16:05 +0300
commitc69894eaec2a29f9db023cbfff63e48266de4ee5 (patch)
tree503a18333c8b8f4a1d968c28154f4c3942df67db /compiler/rustc_lint/src
parent81d6652e741f091f4ee0b7a660120d204e0417b8 (diff)
downloadrust-c69894eaec2a29f9db023cbfff63e48266de4ee5.tar.gz
rust-c69894eaec2a29f9db023cbfff63e48266de4ee5.zip
New lint: `dangling_pointers_from_temporaries`
Diffstat (limited to 'compiler/rustc_lint/src')
-rw-r--r--compiler/rustc_lint/src/dangling.rs223
-rw-r--r--compiler/rustc_lint/src/lib.rs7
-rw-r--r--compiler/rustc_lint/src/lints.rs17
-rw-r--r--compiler/rustc_lint/src/methods.rs69
4 files changed, 237 insertions, 79 deletions
diff --git a/compiler/rustc_lint/src/dangling.rs b/compiler/rustc_lint/src/dangling.rs
new file mode 100644
index 00000000000..a34c3e26778
--- /dev/null
+++ b/compiler/rustc_lint/src/dangling.rs
@@ -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
+    }
+}
diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs
index 5389860e23b..9014d901921 100644
--- a/compiler/rustc_lint/src/lib.rs
+++ b/compiler/rustc_lint/src/lib.rs
@@ -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
diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs
index 16cfae17d40..000f4b697bd 100644
--- a/compiler/rustc_lint/src/lints.rs
+++ b/compiler/rustc_lint/src/lints.rs
@@ -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
diff --git a/compiler/rustc_lint/src/methods.rs b/compiler/rustc_lint/src/methods.rs
deleted file mode 100644
index df22bf0972d..00000000000
--- a/compiler/rustc_lint/src/methods.rs
+++ /dev/null
@@ -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,
-                    });
-                }
-            }
-        }
-    }
-}