Skip to content

Commit ebcbc2d

Browse files
committed
new lint ptr_to_temporary
1 parent d2c9047 commit ebcbc2d

22 files changed

+845
-55
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5163,6 +5163,7 @@ Released 2018-09-13
51635163
[`ptr_cast_constness`]: https://rust-lang.github.io/rust-clippy/master/index.html#ptr_cast_constness
51645164
[`ptr_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#ptr_eq
51655165
[`ptr_offset_with_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#ptr_offset_with_cast
5166+
[`ptr_to_temporary`]: https://rust-lang.github.io/rust-clippy/master/index.html#ptr_to_temporary
51665167
[`pub_enum_variant_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#pub_enum_variant_names
51675168
[`pub_use`]: https://rust-lang.github.io/rust-clippy/master/index.html#pub_use
51685169
[`pub_with_shorthand`]: https://rust-lang.github.io/rust-clippy/master/index.html#pub_with_shorthand

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
544544
crate::ptr::MUT_FROM_REF_INFO,
545545
crate::ptr::PTR_ARG_INFO,
546546
crate::ptr_offset_with_cast::PTR_OFFSET_WITH_CAST_INFO,
547+
crate::ptr_to_temporary::PTR_TO_TEMPORARY_INFO,
547548
crate::pub_use::PUB_USE_INFO,
548549
crate::question_mark::QUESTION_MARK_INFO,
549550
crate::question_mark_used::QUESTION_MARK_USED_INFO,

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![feature(array_windows)]
22
#![feature(binary_heap_into_iter_sorted)]
33
#![feature(box_patterns)]
4+
#![feature(exclusive_range_pattern)]
45
#![feature(if_let_guard)]
56
#![feature(iter_intersperse)]
67
#![feature(let_chains)]
@@ -262,6 +263,7 @@ mod permissions_set_readonly_false;
262263
mod precedence;
263264
mod ptr;
264265
mod ptr_offset_with_cast;
266+
mod ptr_to_temporary;
265267
mod pub_use;
266268
mod question_mark;
267269
mod question_mark_used;
@@ -1082,6 +1084,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10821084
store.register_late_pass(|_| Box::new(manual_float_methods::ManualFloatMethods));
10831085
store.register_late_pass(|_| Box::new(four_forward_slashes::FourForwardSlashes));
10841086
store.register_late_pass(|_| Box::new(error_impl_error::ErrorImplError));
1087+
store.register_late_pass(move |_| Box::new(ptr_to_temporary::PtrToTemporary));
10851088
// add lints here, do not remove this comment, it's used in `new_lint`
10861089
}
10871090

clippy_lints/src/ptr_to_temporary.rs

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
use clippy_utils::consts::is_promotable;
2+
use clippy_utils::diagnostics::{span_lint_and_note, span_lint_hir_and_then};
3+
use clippy_utils::mir::{location_to_node, StatementOrTerminator};
4+
use rustc_data_structures::fx::FxHashMap;
5+
use rustc_hir::def_id::{DefId, LocalDefId};
6+
use rustc_hir::intravisit::FnKind;
7+
use rustc_hir::{Body, BorrowKind, Expr, ExprKind, FnDecl, HirId, ItemKind, OwnerNode};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::mir::visit::{MutatingUseContext, PlaceContext, Visitor};
10+
use rustc_middle::mir::{
11+
self, BasicBlock, BasicBlockData, CallSource, Local, Location, Place, PlaceRef, ProjectionElem, Rvalue, SourceInfo,
12+
StatementKind, TerminatorKind,
13+
};
14+
use rustc_session::{declare_lint_pass, declare_tool_lint};
15+
use rustc_span::{sym, Span, Symbol};
16+
17+
declare_clippy_lint! {
18+
/// ### What it does
19+
/// Checks for raw pointers pointing to temporary values that will **not** be promoted to a
20+
/// constant through
21+
/// [constant promotion](https://doc.rust-lang.org/stable/reference/destructors.html#constant-promotion).
22+
///
23+
/// ### Why is this bad?
24+
/// Usage of such a pointer will result in Undefined Behavior, as the pointer will stop
25+
/// pointing to valid stack memory once the temporary is dropped.
26+
///
27+
/// ### Known problems
28+
/// Expects any call to methods named `as_ptr` or `as_mut_ptr` returning a raw pointer to have
29+
/// that raw pointer point to data owned by self. Essentially, it will lint all temporary
30+
/// `as_ptr` calls even if the pointer doesn't point to the temporary.
31+
///
32+
/// ### Example
33+
/// ```rust,ignore
34+
/// fn returning_temp() -> *const i32 {
35+
/// let x = 0;
36+
/// &x as *const i32
37+
/// }
38+
///
39+
/// let px = returning_temp();
40+
/// unsafe { *px }; // ⚠️
41+
/// let pv = vec![].as_ptr();
42+
/// unsafe { *pv }; // ⚠️
43+
/// ```
44+
#[clippy::version = "1.72.0"]
45+
pub PTR_TO_TEMPORARY,
46+
// TODO: Let's make it warn-by-default for now, and change this to deny-by-default once we know
47+
// there are no major FPs
48+
suspicious,
49+
"disallows obtaining raw pointers to temporary values"
50+
}
51+
declare_lint_pass!(PtrToTemporary => [PTR_TO_TEMPORARY]);
52+
53+
impl<'tcx> LateLintPass<'tcx> for PtrToTemporary {
54+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
55+
check_for_returning_raw_ptr(cx, expr);
56+
}
57+
58+
fn check_fn(
59+
&mut self,
60+
cx: &LateContext<'tcx>,
61+
_: FnKind<'_>,
62+
_: &FnDecl<'_>,
63+
_: &Body<'_>,
64+
_: Span,
65+
def_id: LocalDefId,
66+
) {
67+
let mir = cx.tcx.optimized_mir(def_id);
68+
69+
// Collect all local assignments in this body. This is faster than continuously passing over the
70+
// body every time we want to get the assignments.
71+
let mut assignments = LocalAssignmentsVisitor {
72+
results: FxHashMap::default(),
73+
};
74+
assignments.visit_body(mir);
75+
76+
let mut v = DanglingPtrVisitor {
77+
cx,
78+
body: mir,
79+
results: vec![],
80+
local_assignments: assignments.results,
81+
};
82+
v.visit_body(mir);
83+
84+
for (span, hir_id, ident) in v.results {
85+
// TODO: We need to lint on the call in question instead, so lint attributes work fine. I'm not sure
86+
// how to though
87+
span_lint_hir_and_then(
88+
cx,
89+
PTR_TO_TEMPORARY,
90+
hir_id,
91+
span,
92+
&format!("calling `{ident}` on a temporary value"),
93+
|diag| {
94+
diag.note(
95+
"usage of this pointer will cause Undefined Behavior as the temporary will be deallocated at \
96+
the end of the statement, yet the pointer will continue pointing to it, resulting in a \
97+
dangling pointer",
98+
);
99+
},
100+
);
101+
}
102+
}
103+
}
104+
105+
/// Check for returning raw pointers to temporaries that are not promoted to a constant
106+
fn check_for_returning_raw_ptr<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
107+
// Get the final return statement if this is a return statement, or don't lint
108+
let expr = if let ExprKind::Ret(Some(expr)) = expr.kind {
109+
expr
110+
} else if let OwnerNode::Item(parent) = cx.tcx.hir().owner(cx.tcx.hir().get_parent_item(expr.hir_id))
111+
&& let ItemKind::Fn(_, _, body) = parent.kind
112+
&& let block = cx.tcx.hir().body(body).value
113+
&& let ExprKind::Block(block, _) = block.kind
114+
&& let Some(final_block_expr) = block.expr
115+
&& final_block_expr.hir_id == expr.hir_id
116+
{
117+
expr
118+
} else {
119+
return false;
120+
};
121+
122+
if let ExprKind::Cast(cast_expr, _) = expr.kind
123+
&& let ExprKind::AddrOf(BorrowKind::Ref, _, e) = cast_expr.kind
124+
&& !is_promotable(cx, e)
125+
{
126+
span_lint_and_note(
127+
cx,
128+
PTR_TO_TEMPORARY,
129+
expr.span,
130+
"returning a raw pointer to a temporary value that cannot be promoted to a constant",
131+
None,
132+
"usage of this pointer by callers will cause Undefined Behavior as the temporary will be deallocated at \
133+
the end of the statement, yet the pointer will continue pointing to it, resulting in a dangling pointer",
134+
);
135+
136+
return true;
137+
}
138+
139+
false
140+
}
141+
142+
struct LocalAssignmentsVisitor {
143+
results: FxHashMap<Local, Vec<Location>>,
144+
}
145+
146+
impl Visitor<'_> for LocalAssignmentsVisitor {
147+
fn visit_place(&mut self, place: &Place<'_>, ctxt: PlaceContext, loc: Location) {
148+
if matches!(
149+
ctxt,
150+
PlaceContext::MutatingUse(
151+
MutatingUseContext::Store | MutatingUseContext::Call | MutatingUseContext::Borrow
152+
)
153+
) {
154+
self.results.entry(place.local).or_insert(vec![]).push(loc);
155+
}
156+
}
157+
}
158+
159+
struct DanglingPtrVisitor<'a, 'tcx> {
160+
cx: &'a LateContext<'tcx>,
161+
body: &'tcx mir::Body<'tcx>,
162+
local_assignments: FxHashMap<Local, Vec<Location>>,
163+
results: Vec<(Span, HirId, Symbol)>,
164+
}
165+
166+
impl<'tcx> Visitor<'tcx> for DanglingPtrVisitor<'_, 'tcx> {
167+
fn visit_basic_block_data(&mut self, _: BasicBlock, data: &BasicBlockData<'tcx>) {
168+
let Self {
169+
cx,
170+
body,
171+
local_assignments,
172+
results,
173+
} = self;
174+
175+
if let Some(term) = &data.terminator
176+
&& let TerminatorKind::Call {
177+
func,
178+
args,
179+
destination,
180+
target: Some(target),
181+
call_source: CallSource::Normal,
182+
..
183+
} = &term.kind
184+
&& destination.ty(&body.local_decls, cx.tcx).ty.is_unsafe_ptr()
185+
&& let [recv] = args.as_slice()
186+
&& let Some(recv) = recv.place()
187+
&& let Some((def_id, _)) = func.const_fn_def()
188+
&& let Some(ident) = returns_ptr_to_self(cx, def_id)
189+
&& let Ok(recv) = traverse_up_until_owned(body, local_assignments, recv)
190+
{
191+
192+
check_for_dangling(
193+
body,
194+
*target,
195+
recv,
196+
destination.as_ref(),
197+
term.source_info,
198+
ident,
199+
results,
200+
);
201+
}
202+
}
203+
}
204+
205+
fn check_for_dangling<'tcx>(
206+
body: &mir::Body<'tcx>,
207+
bb: BasicBlock,
208+
mut recv: PlaceRef<'tcx>,
209+
ptr: PlaceRef<'_>,
210+
source_info: SourceInfo,
211+
ident: Symbol,
212+
results: &mut Vec<(Span, HirId, Symbol)>,
213+
) {
214+
let data = &body.basic_blocks[bb];
215+
let mut recv_dead = false;
216+
217+
// If there's a `Drop`, we must include the statements in its target so we don't miss any
218+
// potentially important `StorageDead`s.
219+
let rest = vec![];
220+
let rest = if let Some(term) = &data.terminator && let TerminatorKind::Drop { place, target, .. } = term.kind {
221+
// This indicates a bug in our heuristic. It's normally fine if the `Drop` is present, but
222+
// if it isn't (i.e., no drop glue) then we may have FNs, or worse. Let's catch this early
223+
// if there are upstream MIR changes.
224+
debug_assert_eq!(place.as_ref(), recv, "dropped place is not receiver");
225+
// In release mode, let's prevent a few FPs where `Drop` is present.
226+
recv = place.as_ref();
227+
228+
&body.basic_blocks[target].statements
229+
} else {
230+
&rest
231+
};
232+
233+
for dead_local in data.statements.iter().chain(rest).filter_map(|stmt| {
234+
if let StatementKind::StorageDead(local) = stmt.kind {
235+
return Some(local);
236+
}
237+
238+
None
239+
}) {
240+
match (dead_local == recv.local, dead_local == ptr.local) {
241+
(true, false) => recv_dead = true,
242+
(false, true) if recv_dead => {
243+
results.push((
244+
source_info.span,
245+
body.source_scopes[source_info.scope]
246+
.local_data
247+
.clone()
248+
.assert_crate_local()
249+
.lint_root,
250+
ident,
251+
));
252+
},
253+
_ => continue,
254+
}
255+
}
256+
}
257+
258+
/// Traverses the MIR backwards until it finds owned data. This can be assumed to be the dropped
259+
/// data in the next `Drop` terminator, if not this indicates a bug in our heuristic.
260+
fn traverse_up_until_owned<'tcx>(
261+
body: &'tcx mir::Body<'tcx>,
262+
local_assignments: &FxHashMap<Local, Vec<Location>>,
263+
start: Place<'tcx>,
264+
) -> Result<PlaceRef<'tcx>, TraverseError> {
265+
traverse_up_until_owned_inner(body, local_assignments, start.as_ref(), 0)
266+
}
267+
268+
fn traverse_up_until_owned_inner<'tcx>(
269+
body: &'tcx mir::Body<'tcx>,
270+
local_assignments: &FxHashMap<Local, Vec<Location>>,
271+
current_place: PlaceRef<'tcx>,
272+
depth: usize,
273+
) -> Result<PlaceRef<'tcx>, TraverseError> {
274+
if depth > 100 {
275+
return Err(TraverseError::MaxDepthReached);
276+
}
277+
let Some(current) = local_assignments.get(&current_place.local) else {
278+
return Err(TraverseError::NoAssignments);
279+
};
280+
if current.is_empty() {
281+
return Err(TraverseError::NoAssignments);
282+
}
283+
let [current] = current.as_slice() else {
284+
return Err(TraverseError::TooManyAssignments);
285+
};
286+
let current = location_to_node(body, *current);
287+
let next = match current {
288+
StatementOrTerminator::Statement(stmt) if let StatementKind::Assign(box (_, rvalue)) = &stmt.kind => {
289+
match rvalue {
290+
Rvalue::Use(op) | Rvalue::Cast(_, op, _) => {
291+
let Some(place) = op.place() else {
292+
return Err(TraverseError::LikelyPromoted);
293+
};
294+
// If there's a field access, this is likely to be accessing `.0` on a
295+
// `Unique`. We need a better heuristic for this though, as this may lead to
296+
// some FPs.
297+
if let Some(place) = place.iter_projections().find_map(|proj| {
298+
if matches!(proj.1, ProjectionElem::Field(_, _)) {
299+
return Some(proj.0);
300+
}
301+
None
302+
}) {
303+
return Ok(place);
304+
}
305+
place.as_ref()
306+
}
307+
Rvalue::Ref(_, _, place) => {
308+
if !place.has_deref() {
309+
return Ok(place.as_ref());
310+
}
311+
place.as_ref()
312+
}
313+
// Give up if we can't determine it's dangling with near 100% accuracy
314+
_ => return Err(TraverseError::InvalidOp),
315+
}
316+
}
317+
StatementOrTerminator::Terminator(term) if let TerminatorKind::Call { args, .. } = &term.kind
318+
&& let [arg] = args.as_slice() =>
319+
{
320+
let Some(place) = arg.place() else {
321+
return Err(TraverseError::LikelyPromoted);
322+
};
323+
place.as_ref()
324+
}
325+
// Give up if we can't determine it's dangling with near 100% accuracy
326+
_ => return Err(TraverseError::InvalidOp),
327+
};
328+
329+
traverse_up_until_owned_inner(body, local_assignments, next, depth + 1)
330+
}
331+
332+
enum TraverseError {
333+
NoAssignments,
334+
TooManyAssignments,
335+
MaxDepthReached,
336+
LikelyPromoted,
337+
InvalidOp,
338+
}
339+
340+
/// Whether the call returns a raw pointer to data owned by self, i.e., `as_ptr` and friends. If so,
341+
/// if it's temporary it will be dangling and we should lint it. Returns the name of the call if so.
342+
fn returns_ptr_to_self(cx: &LateContext<'_>, def_id: DefId) ->Option<Symbol> {
343+
let path = cx.tcx.def_path(def_id).data;
344+
345+
if let [.., last] = &*path
346+
&& let Some(ident) = last.data.get_opt_name()
347+
&& (ident == sym::as_ptr || ident == sym!(as_mut_ptr))
348+
{
349+
return Some(ident);
350+
}
351+
352+
// TODO: More checks here. We want to lint most libstd functions that return a pointer that
353+
// aren't named `as_ptr`.
354+
None
355+
}

0 commit comments

Comments
 (0)