Skip to content

Commit 80d2f78

Browse files
committed
Add a new lint for catching unbound lifetimes in return values
This lint is based on some unsoundness found in Libra during security audits. Some function signatures can look sound, but based on the concrete types that end up being used, lifetimes may be unbound instead of anchored to the references of the arguments or the fields of a struct. When combined with unsafe code, this can cause transmuted lifetimes to be not what was intended.
1 parent 43ac941 commit 80d2f78

12 files changed

+313
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,7 @@ Released 2018-09-13
13181318
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
13191319
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
13201320
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
1321+
[`unbound_return_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#unbound_return_lifetimes
13211322
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc
13221323
[`unimplemented`]: https://rust-lang.github.io/rust-clippy/master/index.html#unimplemented
13231324
[`uninit_assumed_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#uninit_assumed_init

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are 346 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are 347 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:
1212

clippy_lints/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ pub mod transmuting_null;
296296
pub mod trivially_copy_pass_by_ref;
297297
pub mod try_err;
298298
pub mod types;
299+
pub mod unbound_return_lifetimes;
299300
pub mod unicode;
300301
pub mod unsafe_removed_from_name;
301302
pub mod unused_io_amount;
@@ -786,6 +787,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
786787
&types::UNIT_CMP,
787788
&types::UNNECESSARY_CAST,
788789
&types::VEC_BOX,
790+
&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES,
789791
&unicode::NON_ASCII_LITERAL,
790792
&unicode::UNICODE_NOT_NFC,
791793
&unicode::ZERO_WIDTH_SPACE,
@@ -953,6 +955,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
953955
store.register_late_pass(|| box mul_add::MulAddCheck);
954956
store.register_late_pass(|| box mut_key::MutableKeyType);
955957
store.register_late_pass(|| box modulo_arithmetic::ModuloArithmetic);
958+
store.register_late_pass(|| box unbound_return_lifetimes::UnboundReturnLifetimes);
956959
store.register_early_pass(|| box reference::DerefAddrOf);
957960
store.register_early_pass(|| box reference::RefInDeref);
958961
store.register_early_pass(|| box double_parens::DoubleParens);
@@ -1326,6 +1329,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
13261329
LintId::of(&types::UNIT_CMP),
13271330
LintId::of(&types::UNNECESSARY_CAST),
13281331
LintId::of(&types::VEC_BOX),
1332+
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
13291333
LintId::of(&unicode::ZERO_WIDTH_SPACE),
13301334
LintId::of(&unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME),
13311335
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
@@ -1577,6 +1581,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
15771581
LintId::of(&types::CAST_PTR_ALIGNMENT),
15781582
LintId::of(&types::CAST_REF_TO_MUT),
15791583
LintId::of(&types::UNIT_CMP),
1584+
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
15801585
LintId::of(&unicode::ZERO_WIDTH_SPACE),
15811586
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
15821587
LintId::of(&unwrap::PANICKING_UNWRAP),
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use if_chain::if_chain;
2+
use rustc::declare_lint_pass;
3+
use rustc::hir::map::Map;
4+
use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
5+
use rustc_hir::intravisit::*;
6+
use rustc_hir::*;
7+
use rustc_session::declare_tool_lint;
8+
9+
use crate::utils::span_lint;
10+
11+
declare_clippy_lint! {
12+
/// **What it does:** Checks for lifetime annotations on return values
13+
/// which might not always get bound.
14+
///
15+
/// **Why is this bad?** If the function contains unsafe code which
16+
/// transmutes to the lifetime, the resulting lifetime may live longer
17+
/// than was intended.
18+
///
19+
/// **Known problems*: None.
20+
///
21+
/// **Example:**
22+
/// ```rust
23+
/// struct WrappedStr(str);
24+
///
25+
/// // Bad: unbound return lifetime causing unsoundness (e.g. when x is String)
26+
/// fn unbound<'a>(x: impl AsRef<str> + 'a) -> &'a WrappedStr {
27+
/// let s = x.as_ref();
28+
/// unsafe { &*(s as *const str as *const WrappedStr) }
29+
/// }
30+
///
31+
/// // Good: bound return lifetime is sound
32+
/// fn bound<'a>(x: &'a str) -> &'a WrappedStr {
33+
/// unsafe { &*(x as *const str as *const WrappedStr) }
34+
/// }
35+
/// ```
36+
pub UNBOUND_RETURN_LIFETIMES,
37+
correctness,
38+
"unbound lifetimes in function return values"
39+
}
40+
41+
declare_lint_pass!(UnboundReturnLifetimes => [UNBOUND_RETURN_LIFETIMES]);
42+
43+
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for UnboundReturnLifetimes {
44+
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item<'tcx>) {
45+
if let ItemKind::Fn(ref sig, ref generics, _id) = item.kind {
46+
check_fn_inner(cx, &sig.decl, generics, None);
47+
}
48+
}
49+
50+
fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx ImplItem<'tcx>) {
51+
if let ImplItemKind::Method(ref sig, _) = item.kind {
52+
let parent_generics = impl_generics(cx, item.hir_id);
53+
check_fn_inner(cx, &sig.decl, &item.generics, parent_generics);
54+
}
55+
}
56+
}
57+
58+
fn check_fn_inner<'a, 'tcx>(
59+
cx: &LateContext<'a, 'tcx>,
60+
decl: &'tcx FnDecl<'tcx>,
61+
generics: &'tcx Generics<'tcx>,
62+
parent_data: Option<(&'tcx Generics<'tcx>, &'tcx Ty<'tcx>)>,
63+
) {
64+
let output_type = if let FunctionRetTy::Return(ref output_type) = decl.output {
65+
output_type
66+
} else {
67+
return;
68+
};
69+
70+
let lt = if let TyKind::Rptr(ref lt, _) = output_type.kind {
71+
lt
72+
} else {
73+
return;
74+
};
75+
76+
if matches!(lt.name, LifetimeName::Param(_)) {
77+
let target_lt = lt;
78+
79+
// check function generics
80+
// the target lifetime parameter must appear on the left of some outlives relation
81+
if lifetime_outlives_something(target_lt, generics) {
82+
return;
83+
}
84+
85+
// check parent generics
86+
// the target lifetime parameter must appear on the left of some outlives relation
87+
if let Some((ref parent_generics, _)) = parent_data {
88+
if lifetime_outlives_something(target_lt, parent_generics) {
89+
return;
90+
}
91+
}
92+
93+
// check type generics
94+
// the target lifetime parameter must be included in the type
95+
if let Some((_, ref parent_ty)) = parent_data {
96+
if TypeVisitor::type_contains_lifetime(parent_ty, target_lt) {
97+
return;
98+
}
99+
}
100+
101+
// check arguments the target lifetime parameter must be included as a
102+
// lifetime of a reference, either directly or through the gneric
103+
// parameters of the argument type.
104+
for input in decl.inputs.iter() {
105+
if TypeVisitor::type_contains_lifetime(input, target_lt) {
106+
return;
107+
}
108+
}
109+
110+
span_lint(
111+
cx,
112+
UNBOUND_RETURN_LIFETIMES,
113+
target_lt.span,
114+
"lifetime is unconstrained",
115+
);
116+
}
117+
}
118+
119+
struct TypeVisitor<'tcx> {
120+
found: bool,
121+
target_lt: &'tcx Lifetime,
122+
}
123+
124+
impl<'tcx> TypeVisitor<'tcx> {
125+
fn type_contains_lifetime(ty: &Ty<'_>, target_lt: &'tcx Lifetime) -> bool {
126+
let mut visitor = TypeVisitor {
127+
found: false,
128+
target_lt,
129+
};
130+
walk_ty(&mut visitor, ty);
131+
visitor.found
132+
}
133+
}
134+
135+
impl<'tcx> Visitor<'tcx> for TypeVisitor<'tcx> {
136+
type Map = Map<'tcx>;
137+
138+
fn visit_lifetime(&mut self, lt: &'tcx Lifetime) {
139+
if lt.name == self.target_lt.name {
140+
self.found = true;
141+
}
142+
}
143+
144+
fn visit_ty(&mut self, ty: &'tcx Ty<'_>) {
145+
match ty.kind {
146+
TyKind::Rptr(ref lt, _) => {
147+
if lt.name == self.target_lt.name {
148+
self.found = true;
149+
}
150+
},
151+
TyKind::Path(ref qpath) => {
152+
if !self.found {
153+
walk_qpath(self, qpath, ty.hir_id, ty.span);
154+
}
155+
},
156+
_ => (),
157+
}
158+
}
159+
160+
fn nested_visit_map(&mut self) -> NestedVisitorMap<'_, Self::Map> {
161+
NestedVisitorMap::None
162+
}
163+
}
164+
165+
fn lifetime_outlives_something<'tcx>(target_lt: &'tcx Lifetime, generics: &'tcx Generics<'tcx>) -> bool {
166+
if let Some(param) = generics.get_named(target_lt.name.ident().name) {
167+
if param.bounds.iter().any(|b| matches!(b, GenericBound::Outlives(_))) {
168+
return true;
169+
}
170+
}
171+
false
172+
}
173+
174+
fn impl_generics<'tcx>(cx: &LateContext<'_, 'tcx>, hir_id: HirId) -> Option<(&'tcx Generics<'tcx>, &'tcx Ty<'tcx>)> {
175+
let parent_impl = cx.tcx.hir().get_parent_item(hir_id);
176+
if_chain! {
177+
if parent_impl != CRATE_HIR_ID;
178+
if let Node::Item(item) = cx.tcx.hir().get(parent_impl);
179+
if let ItemKind::Impl(_, _, _, ref parent_generics, _, ref ty, _) = item.kind;
180+
then {
181+
return Some((parent_generics, ty))
182+
}
183+
}
184+
None
185+
}

src/lintlist/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use lint::Lint;
66
pub use lint::LINT_LEVELS;
77

88
// begin lint list, do not remove this comment, it’s used in `update_lints`
9-
pub const ALL_LINTS: [Lint; 346] = [
9+
pub const ALL_LINTS: [Lint; 347] = [
1010
Lint {
1111
name: "absurd_extreme_comparisons",
1212
group: "correctness",
@@ -2079,6 +2079,13 @@ pub const ALL_LINTS: [Lint; 346] = [
20792079
deprecation: None,
20802080
module: "trait_bounds",
20812081
},
2082+
Lint {
2083+
name: "unbound_return_lifetimes",
2084+
group: "correctness",
2085+
desc: "unbound lifetimes in function return values",
2086+
deprecation: None,
2087+
module: "unbound_return_lifetimes",
2088+
},
20822089
Lint {
20832090
name: "unicode_not_nfc",
20842091
group: "pedantic",

tests/ui/extra_unused_lifetimes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
dead_code,
44
clippy::needless_lifetimes,
55
clippy::needless_pass_by_value,
6-
clippy::trivially_copy_pass_by_ref
6+
clippy::trivially_copy_pass_by_ref,
7+
clippy::unbound_return_lifetimes
78
)]
89
#![warn(clippy::extra_unused_lifetimes)]
910

tests/ui/extra_unused_lifetimes.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
error: this lifetime isn't used in the function definition
2-
--> $DIR/extra_unused_lifetimes.rs:14:14
2+
--> $DIR/extra_unused_lifetimes.rs:15:14
33
|
44
LL | fn unused_lt<'a>(x: u8) {}
55
| ^^
66
|
77
= note: `-D clippy::extra-unused-lifetimes` implied by `-D warnings`
88

99
error: this lifetime isn't used in the function definition
10-
--> $DIR/extra_unused_lifetimes.rs:16:25
10+
--> $DIR/extra_unused_lifetimes.rs:17:25
1111
|
1212
LL | fn unused_lt_transitive<'a, 'b: 'a>(x: &'b u8) {
1313
| ^^
1414

1515
error: this lifetime isn't used in the function definition
16-
--> $DIR/extra_unused_lifetimes.rs:41:10
16+
--> $DIR/extra_unused_lifetimes.rs:42:10
1717
|
1818
LL | fn x<'a>(&self) {}
1919
| ^^
2020

2121
error: this lifetime isn't used in the function definition
22-
--> $DIR/extra_unused_lifetimes.rs:67:22
22+
--> $DIR/extra_unused_lifetimes.rs:68:22
2323
|
2424
LL | fn unused_lt<'a>(x: u8) {}
2525
| ^^

tests/ui/needless_lifetimes.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#![warn(clippy::needless_lifetimes)]
2-
#![allow(dead_code, clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref)]
2+
#![allow(
3+
dead_code,
4+
clippy::needless_pass_by_value,
5+
clippy::trivially_copy_pass_by_ref,
6+
clippy::unbound_return_lifetimes
7+
)]
38

49
fn distinct_lifetimes<'a, 'b>(_x: &'a u8, _y: &'b u8, _z: u8) {}
510

0 commit comments

Comments
 (0)