Skip to content

Commit 9514db6

Browse files
committed
Auto merge of rust-lang#128985 - GrigorenkoPV:instantly-dangling-pointer, r=<try>
Lint against getting pointers from immediately dropped temporaries Fixes rust-lang#123613 ## Changes: 1. New lint: `dangling_pointers_from_temporaries`. Is a generalization of `temporary_cstring_as_ptr` for more types and more ways to get a temporary. 2. `temporary_cstring_as_ptr` is marked as renamed to `dangling_pointers_from_temporaries`. 3. `clippy::temporary_cstring_as_ptr` is marked as renamed to `dangling_pointers_from_temporaries`. 4. Fixed a false positive[^fp] for when the pointer is not actually dangling because of lifetime extension for function/method call arguments. 5. `core::cell::Cell` is now `rustc_diagnostic_item = "Cell"` ## TODO: - [x] ~Add tests for different types~ - [x] ~Add tests for different ways of getting a temporary~ - [x] ~Regroup tests for this and `temporary_cstring_as_ptr`~ - [x] ~Fix a strange ICE when the lint is `allow`ed.~ - [x] ~Check what happens when you `break` with a bound variable from `loop`/`match`/`if`/`block`.~ - [x] ~Document the lint~ - [x] ~Fix tests rust-lang#128985 (comment) - [x] ~Fix clippy~ - [x] ~Fix miri rust-lang#128985 (review) - [x] [Crater run](https://crater.rust-lang.org/ex/pr-128985) - [x] Put a comprehensive list of known false negatives[^fn] into comments. - [ ] Instead of diagnostic items, maybe use lang items or some special attributes? rust-lang#128985 (comment) ## Questions: - [ ] Should we make something like `is_temporary_rvalue` (but not bogus) available in compiler? - [x] ~Should `temporary_cstring_as_ptr` be deprecated? Across an edition boundary?~ - [ ] Instead of manually checking for a list of known methods, maybe add some sort of annotation to those methods in library and check for the presence of that annotation? - [ ] Maybe even introduce some form of primitive lifetimes for pointers and check those in borrow-checker? ## Future improvements: - [ ] Fix false negatives[^fn] - [ ] Add suggestions rust-lang#128985 (comment) ## Known limitations: ### False negatives[^fn]: See the comments in `compiler/rustc_lint/src/dangling.rs` [^fn]: lint **should** be emitted, but **is not** [^fp]: lint **should not** be emitted, but **is**
2 parents 9afe713 + 76c60f3 commit 9514db6

29 files changed

+971
-127
lines changed

compiler/rustc_lint/messages.ftl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,6 @@ lint_crate_name_in_cfg_attr_deprecated =
209209
lint_crate_type_in_cfg_attr_deprecated =
210210
`crate_type` within an `#![cfg_attr]` attribute is deprecated
211211
212-
lint_cstring_ptr = getting the inner pointer of a temporary `CString`
213-
.as_ptr_label = this pointer will be invalid
214-
.unwrap_label = this `CString` is deallocated at the end of the statement, bind it to a variable to extend its lifetime
215-
.note = pointers do not have a lifetime; when calling `as_ptr` the `CString` will be deallocated at the end of the statement because nothing is referencing it as far as the type system is concerned
216-
.help = for more information, see https://doc.rust-lang.org/reference/destructors.html
217-
218212
lint_custom_inner_attribute_unstable = custom inner attributes are unstable
219213
220214
lint_default_hash_types = prefer `{$preferred}` over `{$used}`, it has better performance
@@ -420,6 +414,12 @@ lint_incomplete_include =
420414
421415
lint_inner_macro_attribute_unstable = inner macro attributes are unstable
422416
417+
lint_instantly_dangling = getting a pointer from a temporary `{$ty}` will result in a dangling pointer
418+
.label_ptr = this pointer will immediately be invalid
419+
.label_temporary = this `{$ty}` is deallocated at the end of the statement, bind it to a variable to extend its lifetime
420+
.note = pointers do not have a lifetime; when calling `{$callee}` the `{$ty}` will be deallocated at the end of the statement because nothing is referencing it as far as the type system is concerned
421+
.help = for more information, see https://doc.rust-lang.org/reference/destructors.html
422+
423423
lint_invalid_asm_label_binary = avoid using labels containing only the digits `0` and `1` in inline assembly
424424
.label = use a different label that doesn't start with `0` or `1`
425425
.help = start numbering with `2` instead

compiler/rustc_lint/src/dangling.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
use rustc_hir::{Block, Expr, ExprKind, HirId, LangItem};
2+
use rustc_middle::ty::{Ty, TyCtxt};
3+
use rustc_session::{declare_lint, impl_lint_pass};
4+
use rustc_span::symbol::sym;
5+
6+
use crate::lints::InstantlyDangling;
7+
use crate::{LateContext, LateLintPass, LintContext};
8+
9+
declare_lint! {
10+
/// The `dangling_pointers_from_temporaries` lint detects getting a pointer to data
11+
/// of a temporary that will immediately get dropped.
12+
///
13+
/// ### Example
14+
///
15+
/// ```rust
16+
/// # #![allow(unused)]
17+
/// # unsafe fn use_data(ptr: *const u8) { }
18+
/// fn gather_and_use(bytes: impl Iterator<Item = u8>) {
19+
/// let x: *const u8 = bytes.collect::<Vec<u8>>().as_ptr();
20+
/// unsafe { use_data(x) }
21+
/// }
22+
/// ```
23+
///
24+
/// {{produces}}
25+
///
26+
/// ### Explanation
27+
///
28+
/// Getting a pointer from a temporary value will not prolong its lifetime,
29+
/// which means that the value can be dropped and the allocation freed
30+
/// while the pointer still exists, making the pointer dangling.
31+
/// This is not an error (as far as the type system is concerned)
32+
/// but probably is not what the user intended either.
33+
///
34+
/// If you need stronger guarantees, consider using references instead,
35+
/// as they are statically verified by the borrow-checker to never dangle.
36+
pub DANGLING_POINTERS_FROM_TEMPORARIES,
37+
Warn,
38+
"detects getting a pointer from a temporary"
39+
}
40+
41+
#[derive(Clone, Debug, PartialEq, Eq)]
42+
enum LifetimeExtension {
43+
/// Lifetime extension has not kicked in yet, but it will soon.
44+
/// Example: walking LHS of a function/method call.
45+
EnableLater { after_exit: HirId, until_exit: HirId },
46+
/// Lifetime extension is currently active.
47+
/// Example: walking a function/method call's arguments.
48+
Enable { until_exit: HirId },
49+
/// Temporary disable lifetime extension.
50+
/// Example: statements of a block that is a function/method call's argument.
51+
Disable { until_exit: HirId },
52+
}
53+
54+
#[derive(Clone, Default)]
55+
pub(crate) struct DanglingPointers {
56+
/// Trying to deal with argument lifetime extension.
57+
///
58+
/// This produces a dangling pointer:
59+
/// ```ignore (example)
60+
/// let ptr = CString::new("hello").unwrap().as_ptr();
61+
/// foo(ptr)
62+
/// ```
63+
///
64+
/// But this does not:
65+
/// ```ignore (example)
66+
/// foo(CString::new("hello").unwrap().as_ptr())
67+
/// ```
68+
///
69+
/// But this does:
70+
/// ```ignore (example)
71+
/// foo({ let ptr = CString::new("hello").unwrap().as_ptr(); ptr })
72+
/// ```
73+
///
74+
/// We have to deal with this situation somehow.
75+
///
76+
/// If we were a visitor, we could just keep track of
77+
/// when we enter and exit places where lifetime extension kicks in
78+
/// during visiting/walking and update a boolean flag accordingly.
79+
///
80+
/// But we are not a visitor. We are a LateLintPass.
81+
/// We are not the one who does the visiting & walking
82+
/// and can maintain this state directly in the call stack.
83+
/// But we do get called on every expression there is,
84+
/// both when entering it and exiting from it
85+
/// during our depth-first walk of the tree.
86+
/// So let's try to maintain this context stack explicitly
87+
/// instead of as a part of the call stack.
88+
nested_calls: Vec<LifetimeExtension>,
89+
}
90+
91+
impl_lint_pass!(DanglingPointers => [DANGLING_POINTERS_FROM_TEMPORARIES]);
92+
93+
/// FIXME: false negatives (i.e. the lint is not emitted when it should be)
94+
/// 1. Method calls that are not checked for:
95+
/// - [`temporary_unsafe_cell.get()`][`core::cell::UnsafeCell::get()`]
96+
/// - [`temporary_sync_unsafe_cell.get()`][`core::cell::SyncUnsafeCell::get()`]
97+
/// 2. Ways to get a temporary that are not recognized:
98+
/// - `owning_temporary.field`
99+
/// - `owning_temporary[index]`
100+
/// 3. No checks for ref-to-ptr conversions:
101+
/// - `&raw [mut] temporary`
102+
/// - `&temporary as *(const|mut) _`
103+
/// - `ptr::from_ref(&temporary)` and friends
104+
impl<'tcx> LateLintPass<'tcx> for DanglingPointers {
105+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
106+
if let Some(LifetimeExtension::Enable { .. }) = self.nested_calls.last() {
107+
match expr.kind {
108+
ExprKind::Block(Block { stmts: [.., last_stmt], .. }, _) => self
109+
.nested_calls
110+
.push(LifetimeExtension::Disable { until_exit: last_stmt.hir_id }),
111+
_ => {
112+
tracing::debug!(skip = ?cx.sess().source_map().span_to_snippet(expr.span));
113+
return;
114+
}
115+
}
116+
}
117+
118+
lint_expr(cx, expr);
119+
120+
if let ExprKind::Call(lhs, _args) | ExprKind::MethodCall(_, lhs, _args, _) = expr.kind {
121+
self.nested_calls.push(LifetimeExtension::EnableLater {
122+
after_exit: lhs.hir_id,
123+
until_exit: expr.hir_id,
124+
})
125+
}
126+
}
127+
128+
fn check_expr_post(&mut self, _: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
129+
self.nested_calls.pop_if(|pos| match pos {
130+
LifetimeExtension::Enable { until_exit }
131+
| LifetimeExtension::Disable { until_exit } => expr.hir_id == *until_exit,
132+
133+
&mut LifetimeExtension::EnableLater { after_exit, until_exit } => {
134+
if expr.hir_id == after_exit {
135+
*pos = LifetimeExtension::Enable { until_exit };
136+
};
137+
false
138+
}
139+
});
140+
}
141+
}
142+
143+
fn lint_expr(cx: &LateContext<'_>, expr: &Expr<'_>) {
144+
if let ExprKind::MethodCall(method, receiver, _args, _span) = expr.kind
145+
&& matches!(method.ident.name, sym::as_ptr | sym::as_mut_ptr)
146+
&& is_temporary_rvalue(receiver)
147+
&& let ty = cx.typeck_results().expr_ty(receiver)
148+
&& is_interesting(cx.tcx, ty)
149+
{
150+
cx.emit_span_lint(
151+
DANGLING_POINTERS_FROM_TEMPORARIES,
152+
method.ident.span,
153+
InstantlyDangling {
154+
callee: method.ident.name,
155+
ty,
156+
ptr_span: method.ident.span,
157+
temporary_span: receiver.span,
158+
},
159+
)
160+
}
161+
}
162+
163+
fn is_temporary_rvalue(expr: &Expr<'_>) -> bool {
164+
match expr.kind {
165+
// Const is not temporary.
166+
ExprKind::ConstBlock(..) | ExprKind::Repeat(..) | ExprKind::Lit(..) => false,
167+
168+
// This is literally lvalue.
169+
ExprKind::Path(..) => false,
170+
171+
// Calls return rvalues.
172+
ExprKind::Call(..) | ExprKind::MethodCall(..) | ExprKind::Binary(..) => true,
173+
174+
// Inner blocks are rvalues.
175+
ExprKind::If(..) | ExprKind::Loop(..) | ExprKind::Match(..) | ExprKind::Block(..) => true,
176+
177+
// FIXME: these should probably recurse and typecheck along the way.
178+
// Some false negatives are possible for now.
179+
ExprKind::Index(..) | ExprKind::Field(..) | ExprKind::Unary(..) => false,
180+
181+
ExprKind::Struct(..) => true,
182+
183+
// FIXME: this has false negatives, but I do not want to deal with 'static/const promotion just yet.
184+
ExprKind::Array(..) => false,
185+
186+
// These typecheck to `!`
187+
ExprKind::Break(..) | ExprKind::Continue(..) | ExprKind::Ret(..) | ExprKind::Become(..) => {
188+
false
189+
}
190+
191+
// These typecheck to `()`
192+
ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::Yield(..) => false,
193+
194+
// Compiler-magic macros
195+
ExprKind::AddrOf(..) | ExprKind::OffsetOf(..) | ExprKind::InlineAsm(..) => false,
196+
197+
// We are not interested in these
198+
ExprKind::Cast(..)
199+
| ExprKind::Closure(..)
200+
| ExprKind::Tup(..)
201+
| ExprKind::DropTemps(..)
202+
| ExprKind::Let(..) => false,
203+
204+
// Not applicable
205+
ExprKind::Type(..) | ExprKind::Err(..) => false,
206+
}
207+
}
208+
209+
// Array, Vec, String, CString, MaybeUninit, Cell, Box<[_]>, Box<str>, Box<CStr>,
210+
// or any of the above in arbitrary many nested Box'es.
211+
fn is_interesting(tcx: TyCtxt<'_>, ty: Ty<'_>) -> bool {
212+
if ty.is_array() {
213+
true
214+
} else if let Some(inner) = ty.boxed_ty() {
215+
inner.is_slice()
216+
|| inner.is_str()
217+
|| inner.ty_adt_def().is_some_and(|def| tcx.is_lang_item(def.did(), LangItem::CStr))
218+
|| is_interesting(tcx, inner)
219+
} else if let Some(def) = ty.ty_adt_def() {
220+
for lang_item in [LangItem::String, LangItem::MaybeUninit] {
221+
if tcx.is_lang_item(def.did(), lang_item) {
222+
return true;
223+
}
224+
}
225+
tcx.get_diagnostic_name(def.did())
226+
.is_some_and(|name| matches!(name, sym::cstring_type | sym::Vec | sym::Cell))
227+
} else {
228+
false
229+
}
230+
}

compiler/rustc_lint/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@
4040
#![feature(rustc_attrs)]
4141
#![feature(rustdoc_internals)]
4242
#![feature(trait_upcasting)]
43+
#![feature(vec_pop_if)]
4344
#![warn(unreachable_pub)]
4445
// tidy-alphabetical-end
4546

4647
mod async_closures;
4748
mod async_fn_in_trait;
4849
pub mod builtin;
4950
mod context;
51+
mod dangling;
5052
mod deref_into_dyn_supertrait;
5153
mod drop_forget_useless;
5254
mod early;
@@ -65,7 +67,6 @@ mod levels;
6567
mod lints;
6668
mod macro_expr_fragment_specifier_2024_migration;
6769
mod map_unit_fn;
68-
mod methods;
6970
mod multiple_supertrait_upcastable;
7071
mod non_ascii_idents;
7172
mod non_fmt_panic;
@@ -89,6 +90,7 @@ mod unused;
8990
use async_closures::AsyncClosureUsage;
9091
use async_fn_in_trait::AsyncFnInTrait;
9192
use builtin::*;
93+
use dangling::*;
9294
use deref_into_dyn_supertrait::*;
9395
use drop_forget_useless::*;
9496
use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums;
@@ -100,7 +102,6 @@ use invalid_from_utf8::*;
100102
use let_underscore::*;
101103
use macro_expr_fragment_specifier_2024_migration::*;
102104
use map_unit_fn::*;
103-
use methods::*;
104105
use multiple_supertrait_upcastable::*;
105106
use non_ascii_idents::*;
106107
use non_fmt_panic::NonPanicFmt;
@@ -227,7 +228,7 @@ late_lint_methods!(
227228
UngatedAsyncFnTrackCaller: UngatedAsyncFnTrackCaller,
228229
ShadowedIntoIter: ShadowedIntoIter,
229230
DropTraitConstraints: DropTraitConstraints,
230-
TemporaryCStringAsPtr: TemporaryCStringAsPtr,
231+
DanglingPointers: DanglingPointers::default(),
231232
NonPanicFmt: NonPanicFmt,
232233
NoopMethodCall: NoopMethodCall,
233234
EnumIntrinsicsNonEnums: EnumIntrinsicsNonEnums,
@@ -348,6 +349,7 @@ fn register_builtins(store: &mut LintStore) {
348349
store.register_renamed("non_fmt_panic", "non_fmt_panics");
349350
store.register_renamed("unused_tuple_struct_fields", "dead_code");
350351
store.register_renamed("static_mut_ref", "static_mut_refs");
352+
store.register_renamed("temporary_cstring_as_ptr", "dangling_pointers_from_temporaries");
351353

352354
// These were moved to tool lints, but rustc still sees them when compiling normally, before
353355
// tool lints are registered, so `check_tool_name_for_backwards_compat` doesn't work. Use

compiler/rustc_lint/src/lints.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,16 +1136,19 @@ pub(crate) struct IgnoredUnlessCrateSpecified<'a> {
11361136
pub name: Symbol,
11371137
}
11381138

1139-
// methods.rs
1139+
// dangling.rs
11401140
#[derive(LintDiagnostic)]
1141-
#[diag(lint_cstring_ptr)]
1141+
#[diag(lint_instantly_dangling)]
11421142
#[note]
11431143
#[help]
1144-
pub(crate) struct CStringPtr {
1145-
#[label(lint_as_ptr_label)]
1146-
pub as_ptr: Span,
1147-
#[label(lint_unwrap_label)]
1148-
pub unwrap: Span,
1144+
// FIXME: use #[primary_span]
1145+
pub(crate) struct InstantlyDangling<'tcx> {
1146+
pub callee: Symbol,
1147+
pub ty: Ty<'tcx>,
1148+
#[label(lint_label_ptr)]
1149+
pub ptr_span: Span,
1150+
#[label(lint_label_temporary)]
1151+
pub temporary_span: Span,
11491152
}
11501153

11511154
// multiple_supertrait_upcastable.rs

0 commit comments

Comments
 (0)