about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Tardieu <sam@rfc1149.net>2025-06-01 09:48:27 +0000
committerGitHub <noreply@github.com>2025-06-01 09:48:27 +0000
commit1652187a03a137fe20ec05ae48b5b74f862dc0a9 (patch)
treea8bb8caa183d80bf6e432a01512853309f2f6910
parent57cbadd68ac473bc50453f6b1320a02b68115f12 (diff)
parente64dd8ed96c7593da92c3d3cf3603a1799064c39 (diff)
downloadrust-1652187a03a137fe20ec05ae48b5b74f862dc0a9.tar.gz
rust-1652187a03a137fe20ec05ae48b5b74f862dc0a9.zip
new restriction lint: pointer_format (#14792)
I read a blog post about kernel security, and how various features might
get lost while porting to Rust. In kernel C, they have some guardrails
against divulging pointers. An easy way to replicate that in Rust is a
lint for pointer formatting. So that's what this lint does.

---

changelog: new [`pointer_format`] lint
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/format_args.rs112
-rw-r--r--tests/ui/pointer_format.rs66
-rw-r--r--tests/ui/pointer_format.stderr47
5 files changed, 223 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a98217f625..97a8fdedc4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6163,6 +6163,7 @@ Released 2018-09-13
 [`pathbuf_init_then_push`]: https://rust-lang.github.io/rust-clippy/master/index.html#pathbuf_init_then_push
 [`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
 [`permissions_set_readonly_false`]: https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false
+[`pointer_format`]: https://rust-lang.github.io/rust-clippy/master/index.html#pointer_format
 [`pointers_in_nomem_asm_block`]: https://rust-lang.github.io/rust-clippy/master/index.html#pointers_in_nomem_asm_block
 [`positional_named_format_parameters`]: https://rust-lang.github.io/rust-clippy/master/index.html#positional_named_format_parameters
 [`possible_missing_comma`]: https://rust-lang.github.io/rust-clippy/master/index.html#possible_missing_comma
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 5fcb851dfeb..4e6ee06d035 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -166,6 +166,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::floating_point_arithmetic::SUBOPTIMAL_FLOPS_INFO,
     crate::format::USELESS_FORMAT_INFO,
     crate::format_args::FORMAT_IN_FORMAT_ARGS_INFO,
+    crate::format_args::POINTER_FORMAT_INFO,
     crate::format_args::TO_STRING_IN_FORMAT_ARGS_INFO,
     crate::format_args::UNINLINED_FORMAT_ARGS_INFO,
     crate::format_args::UNNECESSARY_DEBUG_FORMATTING_INFO,
diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs
index a26e736c7ae..ab6ae91a00f 100644
--- a/clippy_lints/src/format_args.rs
+++ b/clippy_lints/src/format_args.rs
@@ -1,6 +1,8 @@
+use std::collections::hash_map::Entry;
+
 use arrayvec::ArrayVec;
 use clippy_config::Conf;
-use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
+use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
 use clippy_utils::macros::{
     FormatArgsStorage, FormatParamUsage, MacroCall, find_format_arg_expr, format_arg_removal_span,
     format_placeholder_format_span, is_assert_macro, is_format_macro, is_panic, matching_root_macro_call,
@@ -22,10 +24,12 @@ use rustc_errors::SuggestionStyle::{CompletelyHidden, ShowCode};
 use rustc_hir::{Expr, ExprKind, LangItem};
 use rustc_lint::{LateContext, LateLintPass, LintContext};
 use rustc_middle::ty::adjustment::{Adjust, Adjustment};
-use rustc_middle::ty::{List, Ty, TyCtxt};
+use rustc_middle::ty::{self, GenericArg, List, TraitRef, Ty, TyCtxt, Upcast};
 use rustc_session::impl_lint_pass;
 use rustc_span::edition::Edition::Edition2021;
 use rustc_span::{Span, Symbol, sym};
+use rustc_trait_selection::infer::TyCtxtInferExt;
+use rustc_trait_selection::traits::{Obligation, ObligationCause, Selection, SelectionContext};
 
 declare_clippy_lint! {
     /// ### What it does
@@ -194,12 +198,41 @@ declare_clippy_lint! {
     "use of a format specifier that has no effect"
 }
 
+declare_clippy_lint! {
+    /// ### What it does
+    /// Detects [pointer format] as well as `Debug` formatting of raw pointers or function pointers
+    /// or any types that have a derived `Debug` impl that recursively contains them.
+    ///
+    /// ### Why restrict this?
+    /// The addresses are only useful in very specific contexts, and certain projects may want to keep addresses of
+    /// certain data structures or functions from prying hacker eyes as an additional line of security.
+    ///
+    /// ### Known problems
+    /// The lint currently only looks through derived `Debug` implementations. Checking whether a manual
+    /// implementation prints an address is left as an exercise to the next lint implementer.
+    ///
+    /// ### Example
+    /// ```no_run
+    /// let foo = &0_u32;
+    /// fn bar() {}
+    /// println!("{:p}", foo);
+    /// let _ = format!("{:?}", &(bar as fn()));
+    /// ```
+    ///
+    /// [pointer format]: https://doc.rust-lang.org/std/fmt/index.html#formatting-traits
+    #[clippy::version = "1.88.0"]
+    pub POINTER_FORMAT,
+    restriction,
+    "formatting a pointer"
+}
+
 impl_lint_pass!(FormatArgs<'_> => [
     FORMAT_IN_FORMAT_ARGS,
     TO_STRING_IN_FORMAT_ARGS,
     UNINLINED_FORMAT_ARGS,
     UNNECESSARY_DEBUG_FORMATTING,
     UNUSED_FORMAT_SPECS,
+    POINTER_FORMAT,
 ]);
 
 #[allow(clippy::struct_field_names)]
@@ -208,6 +241,8 @@ pub struct FormatArgs<'tcx> {
     msrv: Msrv,
     ignore_mixed: bool,
     ty_msrv_map: FxHashMap<Ty<'tcx>, Option<RustcVersion>>,
+    has_derived_debug: FxHashMap<Ty<'tcx>, bool>,
+    has_pointer_format: FxHashMap<Ty<'tcx>, bool>,
 }
 
 impl<'tcx> FormatArgs<'tcx> {
@@ -218,6 +253,8 @@ impl<'tcx> FormatArgs<'tcx> {
             msrv: conf.msrv,
             ignore_mixed: conf.allow_mixed_uninlined_format_args,
             ty_msrv_map,
+            has_derived_debug: FxHashMap::default(),
+            has_pointer_format: FxHashMap::default(),
         }
     }
 }
@@ -228,7 +265,7 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> {
             && is_format_macro(cx, macro_call.def_id)
             && let Some(format_args) = self.format_args.get(cx, expr, macro_call.expn)
         {
-            let linter = FormatArgsExpr {
+            let mut linter = FormatArgsExpr {
                 cx,
                 expr,
                 macro_call: &macro_call,
@@ -236,6 +273,8 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> {
                 ignore_mixed: self.ignore_mixed,
                 msrv: &self.msrv,
                 ty_msrv_map: &self.ty_msrv_map,
+                has_derived_debug: &mut self.has_derived_debug,
+                has_pointer_format: &mut self.has_pointer_format,
             };
 
             linter.check_templates();
@@ -255,10 +294,12 @@ struct FormatArgsExpr<'a, 'tcx> {
     ignore_mixed: bool,
     msrv: &'a Msrv,
     ty_msrv_map: &'a FxHashMap<Ty<'tcx>, Option<RustcVersion>>,
+    has_derived_debug: &'a mut FxHashMap<Ty<'tcx>, bool>,
+    has_pointer_format: &'a mut FxHashMap<Ty<'tcx>, bool>,
 }
 
 impl<'tcx> FormatArgsExpr<'_, 'tcx> {
-    fn check_templates(&self) {
+    fn check_templates(&mut self) {
         for piece in &self.format_args.template {
             if let FormatArgsPiece::Placeholder(placeholder) = piece
                 && let Ok(index) = placeholder.argument.index
@@ -279,6 +320,17 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> {
                 if placeholder.format_trait == FormatTrait::Debug {
                     let name = self.cx.tcx.item_name(self.macro_call.def_id);
                     self.check_unnecessary_debug_formatting(name, arg_expr);
+                    if let Some(span) = placeholder.span
+                        && self.has_pointer_debug(self.cx.typeck_results().expr_ty(arg_expr), 0)
+                    {
+                        span_lint(self.cx, POINTER_FORMAT, span, "pointer formatting detected");
+                    }
+                }
+
+                if placeholder.format_trait == FormatTrait::Pointer
+                    && let Some(span) = placeholder.span
+                {
+                    span_lint(self.cx, POINTER_FORMAT, span, "pointer formatting detected");
                 }
             }
         }
@@ -559,6 +611,58 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> {
 
         false
     }
+
+    fn has_pointer_debug(&mut self, ty: Ty<'tcx>, depth: usize) -> bool {
+        let cx = self.cx;
+        let tcx = cx.tcx;
+        if !tcx.recursion_limit().value_within_limit(depth) {
+            return false;
+        }
+        let depth = depth + 1;
+        let typing_env = cx.typing_env();
+        let ty = tcx.normalize_erasing_regions(typing_env, ty);
+        match ty.kind() {
+            ty::RawPtr(..) | ty::FnPtr(..) | ty::FnDef(..) => true,
+            ty::Ref(_, t, _) | ty::Slice(t) | ty::Array(t, _) => self.has_pointer_debug(*t, depth),
+            ty::Tuple(ts) => ts.iter().any(|t| self.has_pointer_debug(t, depth)),
+            ty::Adt(adt, args) => {
+                match self.has_pointer_format.entry(ty) {
+                    Entry::Occupied(o) => return *o.get(),
+                    Entry::Vacant(v) => v.insert(false),
+                };
+                let derived_debug = if let Some(&known) = self.has_derived_debug.get(&ty) {
+                    known
+                } else {
+                    let Some(trait_id) = tcx.get_diagnostic_item(sym::Debug) else {
+                        return false;
+                    };
+                    let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(typing_env);
+                    let trait_ref = TraitRef::new(tcx, trait_id, [GenericArg::from(ty)]);
+                    let obligation = Obligation {
+                        cause: ObligationCause::dummy(),
+                        param_env,
+                        recursion_depth: 0,
+                        predicate: trait_ref.upcast(tcx),
+                    };
+                    let selection = SelectionContext::new(&infcx).select(&obligation);
+                    let derived = if let Ok(Some(Selection::UserDefined(data))) = selection {
+                        tcx.has_attr(data.impl_def_id, sym::automatically_derived)
+                    } else {
+                        false
+                    };
+                    self.has_derived_debug.insert(ty, derived);
+                    derived
+                };
+                let pointer_debug = derived_debug
+                    && adt.all_fields().any(|f| {
+                        self.has_pointer_debug(tcx.normalize_erasing_regions(typing_env, f.ty(tcx, args)), depth)
+                    });
+                self.has_pointer_format.insert(ty, pointer_debug);
+                pointer_debug
+            },
+            _ => false,
+        }
+    }
 }
 
 fn make_ty_msrv_map(tcx: TyCtxt<'_>) -> FxHashMap<Ty<'_>, Option<RustcVersion>> {
diff --git a/tests/ui/pointer_format.rs b/tests/ui/pointer_format.rs
new file mode 100644
index 00000000000..0621f966ad1
--- /dev/null
+++ b/tests/ui/pointer_format.rs
@@ -0,0 +1,66 @@
+#![warn(clippy::pointer_format)]
+
+use core::fmt::Debug;
+use core::marker::PhantomData;
+
+#[derive(Debug)]
+struct ContainsPointerDeep {
+    w: WithPointer,
+}
+
+struct ManualDebug {
+    ptr: *const u8,
+}
+
+#[derive(Debug)]
+struct WithPointer {
+    len: usize,
+    ptr: *const u8,
+}
+
+impl std::fmt::Debug for ManualDebug {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        f.write_str("ManualDebug")
+    }
+}
+
+trait Foo {
+    type Assoc: Foo + Debug;
+}
+
+#[derive(Debug)]
+struct S<T: Foo + 'static>(&'static S<T::Assoc>, PhantomData<T>);
+
+#[allow(unused)]
+fn unbounded<T: Foo + Debug + 'static>(s: &S<T>) {
+    format!("{s:?}");
+}
+
+fn main() {
+    let m = &(main as fn());
+    let g = &0;
+    let o = &format!("{m:p}");
+    //~^ pointer_format
+    let _ = format!("{m:?}");
+    //~^ pointer_format
+    println!("{g:p}");
+    //~^ pointer_format
+    panic!("{o:p}");
+    //~^ pointer_format
+    let answer = 42;
+    let x = &raw const answer;
+    let arr = [0u8; 8];
+    let with_ptr = WithPointer { len: 8, ptr: &arr as _ };
+    let _ = format!("{x:?}");
+    //~^ pointer_format
+    print!("{with_ptr:?}");
+    //~^ pointer_format
+    let container = ContainsPointerDeep { w: with_ptr };
+    print!("{container:?}");
+    //~^ pointer_format
+
+    let no_pointer = "foo";
+    println!("{no_pointer:?}");
+    let manual_debug = ManualDebug { ptr: &arr as _ };
+    println!("{manual_debug:?}");
+}
diff --git a/tests/ui/pointer_format.stderr b/tests/ui/pointer_format.stderr
new file mode 100644
index 00000000000..21ba39b8f8c
--- /dev/null
+++ b/tests/ui/pointer_format.stderr
@@ -0,0 +1,47 @@
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:42:23
+   |
+LL |     let o = &format!("{m:p}");
+   |                       ^^^^^
+   |
+   = note: `-D clippy::pointer-format` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::pointer_format)]`
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:44:22
+   |
+LL |     let _ = format!("{m:?}");
+   |                      ^^^^^
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:46:15
+   |
+LL |     println!("{g:p}");
+   |               ^^^^^
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:48:13
+   |
+LL |     panic!("{o:p}");
+   |             ^^^^^
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:54:22
+   |
+LL |     let _ = format!("{x:?}");
+   |                      ^^^^^
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:56:13
+   |
+LL |     print!("{with_ptr:?}");
+   |             ^^^^^^^^^^^^
+
+error: pointer formatting detected
+  --> tests/ui/pointer_format.rs:59:13
+   |
+LL |     print!("{container:?}");
+   |             ^^^^^^^^^^^^^
+
+error: aborting due to 7 previous errors
+