Skip to content

Commit c3c1f58

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 b245fbd commit c3c1f58

12 files changed

+255
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,7 @@ Released 2018-09-13
12261226
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
12271227
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
12281228
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
1229+
[`unbound_return_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#unbound_return_lifetimes
12291230
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc
12301231
[`unimplemented`]: https://rust-lang.github.io/rust-clippy/master/index.html#unimplemented
12311232
[`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 340 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are 341 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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#![feature(crate_visibility_modifier)]
1414
#![feature(concat_idents)]
1515
#![feature(result_map_or)]
16+
#![feature(matches_macro)]
1617

1718
// FIXME: switch to something more ergonomic here, once available.
1819
// (Currently there is no way to opt into sysroot crates without `extern crate`.)
@@ -290,6 +291,7 @@ pub mod transmuting_null;
290291
pub mod trivially_copy_pass_by_ref;
291292
pub mod try_err;
292293
pub mod types;
294+
pub mod unbound_return_lifetimes;
293295
pub mod unicode;
294296
pub mod unsafe_removed_from_name;
295297
pub mod unused_io_amount;
@@ -770,6 +772,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
770772
&types::UNIT_CMP,
771773
&types::UNNECESSARY_CAST,
772774
&types::VEC_BOX,
775+
&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES,
773776
&unicode::NON_ASCII_LITERAL,
774777
&unicode::UNICODE_NOT_NFC,
775778
&unicode::ZERO_WIDTH_SPACE,
@@ -937,6 +940,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
937940
store.register_late_pass(|| box trait_bounds::TraitBounds);
938941
store.register_late_pass(|| box comparison_chain::ComparisonChain);
939942
store.register_late_pass(|| box mul_add::MulAddCheck);
943+
store.register_late_pass(|| box unbound_return_lifetimes::UnboundReturnLifetimes);
940944
store.register_early_pass(|| box reference::DerefAddrOf);
941945
store.register_early_pass(|| box reference::RefInDeref);
942946
store.register_early_pass(|| box double_parens::DoubleParens);
@@ -1300,6 +1304,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
13001304
LintId::of(&types::UNIT_CMP),
13011305
LintId::of(&types::UNNECESSARY_CAST),
13021306
LintId::of(&types::VEC_BOX),
1307+
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
13031308
LintId::of(&unicode::ZERO_WIDTH_SPACE),
13041309
LintId::of(&unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME),
13051310
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
@@ -1547,6 +1552,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
15471552
LintId::of(&types::CAST_PTR_ALIGNMENT),
15481553
LintId::of(&types::CAST_REF_TO_MUT),
15491554
LintId::of(&types::UNIT_CMP),
1555+
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
15501556
LintId::of(&unicode::ZERO_WIDTH_SPACE),
15511557
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
15521558
LintId::of(&unwrap::PANICKING_UNWRAP),
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use if_chain::if_chain;
2+
use rustc::declare_lint_pass;
3+
use rustc::hir::*;
4+
use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
5+
use rustc_session::declare_tool_lint;
6+
7+
use crate::utils::span_lint;
8+
9+
declare_clippy_lint! {
10+
/// **What it does:** Checks for lifetime annotations on return values
11+
/// which might not always get bound.
12+
///
13+
/// **Why is this bad?** If the function contains unsafe code which
14+
/// transmutes to the lifetime, the resulting lifetime may live longer
15+
/// than was intended.
16+
///
17+
/// **Known problems*: None.
18+
///
19+
/// **Example:**
20+
/// ```rust
21+
/// struct WrappedStr(str);
22+
///
23+
/// // Bad: unbound return lifetime causing unsoundness (e.g. when x is String)
24+
/// fn unbound<'a>(x: impl AsRef<str> + 'a) -> &'a WrappedStr {
25+
/// let s = x.as_ref();
26+
/// unsafe { &*(s as *const str as *const WrappedStr) }
27+
/// }
28+
///
29+
/// // Good: bound return lifetime is sound
30+
/// fn bound<'a>(x: &'a str) -> &'a WrappedStr {
31+
/// unsafe { &*(x as *const str as *const WrappedStr) }
32+
/// }
33+
/// ```
34+
pub UNBOUND_RETURN_LIFETIMES,
35+
correctness,
36+
"unbound lifetimes in function return values"
37+
}
38+
39+
declare_lint_pass!(UnboundReturnLifetimes => [UNBOUND_RETURN_LIFETIMES]);
40+
41+
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for UnboundReturnLifetimes {
42+
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item) {
43+
if let ItemKind::Fn(ref sig, ref generics, _id) = item.kind {
44+
check_fn_inner(cx, &sig.decl, generics, None);
45+
}
46+
}
47+
48+
fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx ImplItem) {
49+
if let ImplItemKind::Method(ref sig, _) = item.kind {
50+
let parent_generics = impl_generics(cx, item.hir_id);
51+
check_fn_inner(cx, &sig.decl, &item.generics, parent_generics);
52+
}
53+
}
54+
}
55+
56+
fn check_fn_inner<'a, 'tcx>(
57+
cx: &LateContext<'a, 'tcx>,
58+
decl: &'tcx FnDecl,
59+
generics: &'tcx Generics,
60+
parent_generics: Option<(&'tcx Generics, Option<&'tcx Generics>, Option<&'tcx Generics>)>,
61+
) {
62+
let output_type = if let FunctionRetTy::Return(ref output_type) = decl.output {
63+
output_type
64+
} else {
65+
return;
66+
};
67+
68+
let lifetime = if let TyKind::Rptr(ref lifetime, _) = output_type.kind {
69+
lifetime
70+
} else {
71+
return;
72+
};
73+
74+
if matches!(lifetime.name, LifetimeName::Param(_)) {
75+
let target_lifetime = lifetime;
76+
77+
// check function generics
78+
// the target lifetime parameter must appear on the left of some outlives relation
79+
if lifetime_outlives_something(target_lifetime, generics) {
80+
return;
81+
}
82+
83+
// check parent generics
84+
// the target lifetime parameter must appear on the left of some outlives relation
85+
if let Some((ref parent_generics, _, _)) = parent_generics {
86+
if lifetime_outlives_something(target_lifetime, parent_generics) {
87+
return;
88+
}
89+
}
90+
91+
// check type generics
92+
// the target lifetime parameter must be included in the struct
93+
if let Some((_, _, Some(ref typ_generics))) = parent_generics {
94+
if typ_generics.get_named(target_lifetime.name.ident().name).is_some() {
95+
return;
96+
}
97+
}
98+
99+
// check arguments
100+
// the target lifetime parameter must be included as a lifetime of a reference
101+
for input in decl.inputs.iter() {
102+
if let TyKind::Rptr(ref lifetime, _) = input.kind {
103+
if lifetime.name == target_lifetime.name {
104+
return;
105+
}
106+
}
107+
}
108+
109+
span_lint(
110+
cx,
111+
UNBOUND_RETURN_LIFETIMES,
112+
target_lifetime.span,
113+
"lifetime is unconstrained",
114+
);
115+
}
116+
}
117+
118+
fn lifetime_outlives_something<'tcx>(target_lifetime: &'tcx Lifetime, generics: &'tcx Generics) -> bool {
119+
if let Some(param) = generics.get_named(target_lifetime.name.ident().name) {
120+
if param
121+
.bounds
122+
.iter()
123+
.any(|b| matches!(b, GenericBound::Outlives(_)))
124+
{
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
131+
fn impl_generics<'tcx>(
132+
cx: &LateContext<'_, 'tcx>,
133+
hir_id: HirId,
134+
) -> Option<(&'tcx Generics, Option<&'tcx Generics>, Option<&'tcx Generics>)> {
135+
let parent_impl = cx.tcx.hir().get_parent_item(hir_id);
136+
if_chain! {
137+
if parent_impl != CRATE_HIR_ID;
138+
if let Node::Item(item) = cx.tcx.hir().get(parent_impl);
139+
if let ItemKind::Impl(_, _, _, ref parent_generics, ref _trait_ref, ref ty, _) = item.kind;
140+
then {
141+
if let TyKind::Path(ref qpath) = ty.kind {
142+
if let Some(typ_def_id) = cx.tables.qpath_res(qpath, ty.hir_id).opt_def_id() {
143+
let typ_generics = cx.tcx.hir().get_generics(typ_def_id);
144+
return Some((parent_generics, None, typ_generics))
145+
}
146+
}
147+
}
148+
}
149+
None
150+
}

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; 340] = [
9+
pub const ALL_LINTS: [Lint; 341] = [
1010
Lint {
1111
name: "absurd_extreme_comparisons",
1212
group: "correctness",
@@ -2037,6 +2037,13 @@ pub const ALL_LINTS: [Lint; 340] = [
20372037
deprecation: None,
20382038
module: "trait_bounds",
20392039
},
2040+
Lint {
2041+
name: "unbound_return_lifetimes",
2042+
group: "correctness",
2043+
desc: "unbound lifetimes in function return values",
2044+
deprecation: None,
2045+
module: "unbound_return_lifetimes",
2046+
},
20402047
Lint {
20412048
name: "unicode_not_nfc",
20422049
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)