-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a new lint for catching unbound lifetimes in return values #4908
Conversation
For future reference: diem/diem#1949 is the PR which fixed the mentioned bug. |
/// struct WrappedStr(str); | ||
/// fn foo<'a>(x: &'a str) -> &'a WrappedStr { | ||
/// unsafe { &*(x as *const str as *const WrappedStr) } | ||
/// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move WrappedStr
above the Bad
comment and use it in bad function as return type for more symmetry between the two examples? Also could you please separate the }
and //
with a newline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also say that when the bad foo takes a String
, 'a
can be equal to 'static
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an example note.
4e321b6
to
6e5fdee
Compare
if param | ||
.bounds | ||
.iter() | ||
.any(|b| if let GenericBound::Outlives(_) = b { true } else { false }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.any(|b| if let GenericBound::Outlives(_) = b { true } else { false }) | |
.any(|b| matches!(b, GenericBound::Outlives(_))) |
Don't forge to append #![feature(matches_macro)]
in crate root.
if let Some(param) = generics.get_named(target_lifetime.name.ident().name) { | ||
if param | ||
.bounds | ||
.iter() | ||
.any(|b| if let GenericBound::Outlives(_) = b { true } else { false }) | ||
{ | ||
return; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block and below one is similar. Maybe split it to a closure or function.
generics: &'tcx Generics, | ||
parent_generics: Option<(&'tcx Generics, Option<&'tcx Generics>, Option<&'tcx Generics>)>, | ||
) { | ||
if let FunctionRetTy::Return(ref typ) = decl.output { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if let FunctionRetTy::Return(ref typ) = decl.output { | |
let output_type = if let FunctionRetTy::Return(ref output_type) = decl.output { | |
output_type | |
} else { | |
return; | |
}; |
This reduces indentation of the code below.
parent_generics: Option<(&'tcx Generics, Option<&'tcx Generics>, Option<&'tcx Generics>)>, | ||
) { | ||
if let FunctionRetTy::Return(ref typ) = decl.output { | ||
if let TyKind::Rptr(ref lifetime, _) = typ.kind { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
c3c1f58
to
4744356
Compare
Updated based on review comments and rebased onto master. |
5a06127
to
df9e86c
Compare
This is a great lint but is it actually detecting unbound lifetimes? According to the Rustonomicon, "Given a function, any output lifetimes that don't derive from inputs are unbounded." In the example -- I tested this lint on the top 50 most downloaded crates on crates.io. I got two warnings which seem to be false positives. Neither of them use unsafe code. regex
unicase
|
No; the bound in |
I can confirm that both https://github.com/rust-lang/regex/blob/e36670a90585b940b0d4f780a053c6db1be7b863/src/re_unicode.rs#L62 and https://github.com/seanmonstar/unicase/blob/7b116bc70c16b972a06c2e084ad1e9f3edfbaa2f/src/lib.rs#L298 are false positives. There should be enough information available to Clippy to make the lint not trigger in these cases. Separately, it may be worth considering only triggering this lint if the function body contains an unsafe block. |
☔ The latest upstream changes (presumably #4930) made this pull request unmergeable. Please resolve the merge conflicts. |
☔ The latest upstream changes (presumably #4885) made this pull request unmergeable. Please resolve the merge conflicts. |
@dtolnay I'd lint all cases, even those without unsafe code, as you cannot be sure if there is unsafe code calling into the function elsewhere. |
@llogiq it's your call, but I feel quite strongly that this lint would be better to trigger only on functions containing unsafe code. Clippy's most severe flaw in my experience has been noisy low-signal lints that are enabled by default (and I have deleted or downgraded several). This lint's point is to catch memory unsafety from incorrectly constrained output lifetimes. A function containing only safe code isn't going to have that memory unsafety, regardless of whether it is called from a different function that is unsafe. I think this lint is too important to make it low-signal. Regarding the false positives:
|
@dtolnay fair enough. I believe the false positives are due to the lint checking HIR types, which don't carry typeck information. Getting the ty::Ty's instead will likely solve the problem. |
I'll look into updating the PR to fix these false positives. @mikerite Did you run clippy on those 50 crates manually, or is there some way to do that automatically? I was looking around for a good way to do that and hadn't come across anything yet. |
You can use our integration testing framework for this:
runs clippy on serde for example. Note, that this suppresses the output of Clippy. You can get the output, by just adding a rust-clippy/tests/integration.rs Line 45 in 50403de
To test your use case, I also recommend to only enable the lint, you want to test here: rust-clippy/tests/integration.rs Lines 39 to 40 in 50403de
( |
df9e86c
to
d64a77b
Compare
I found a couple of cases where I wasn't properly looking at all the parent impl type's lifetimes, so I rewrote the code to use visitors which fixed the problem and simplified the code quite a bit. I've also rebased this on master. With these changes rust-lang/regex and seanmonstar/unicase are both clean. |
@flip1995 I cannot seem to get the integration test to ever fail. I can check out a crate myself, see that my lint fails on it, and then run the integration test on the same crate and watch it succeed. The println you suggested does not seem cause any output to show up running the test. |
The integration test is supposed not to fail on lint failures, since it only checks for ICEs. I just suggested this as a hack, I don't know how @mikerite did this. Do you get any output when adding the |
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.
d64a77b
to
80d2f78
Compare
I found an old internals thread where this example of a safe unbound lifetime was given:
Does this get linted? I can't check right now because by master toolchain installation is broken.
I have a really ugly app that scrapes crates.io for the top crates, clones the source with git and runs clippy on it. I'd have to seriously clean it up before I'd feel comfortable sharing it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't follow this PR very closely. @llogiq can you review it, please?
Only thing, I noticed is the tests/ui/unbound_return_lifetimes.stdout
file, which should be deleted, before merging.
I'll see if I find the time for a review tomorrow. |
let lt = if let TyKind::Rptr(ref lt, _) = output_type.kind { | ||
lt | ||
} else { | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about lifetimes in the output type generics, e.g. Cow<'x, str>
? Shouldn't those be hamdled, too?
Also what about arrays or tuples containing refs?
|
||
struct FooStr(str); | ||
|
||
fn unbound_fun<'a>(x: impl AsRef<str> + 'a) -> &'a FooStr { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see a test with a &'static _
return type, to rule out false positives.
cx: &LateContext<'a, 'tcx>, | ||
decl: &'tcx FnDecl<'tcx>, | ||
generics: &'tcx Generics<'tcx>, | ||
parent_data: Option<(&'tcx Generics<'tcx>, &'tcx Ty<'tcx>)>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can inner functions refer to the lifetimes of outer functions? If yes, going one level up the stack might not be enough.
fn bound_fun<'a>(x: &'a str) -> &'a FooStr { | ||
unsafe { &*(x as *const str as *const FooStr) } | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also perhaps some methods which return tuples or opaque types (impl trait) would be nice.
K: Hash + PartialEq, | ||
{ | ||
pub fn or_insert(&self, key: K, value: V) -> &'a V { | ||
unreachable!() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also how about a function where the output lifetime is wrong (say it should be 'a
, but is 'b
, which is unbound)?
☔ The latest upstream changes (presumably #5067) made this pull request unmergeable. Please resolve the merge conflicts. |
Hi @metajack, are you still up to finish this PR? Let us know if you have any further questions =) |
Yes, I've just been busy with other stuff. I'll put some time aside soon to get this over the finish line. |
Thanks for contributing to Clippy! Sadly this PR was not updated in quite some time. If you waited on input from a reviewer, we're sorry that this fell under the radar. If you want to continue to work on this, just reopen the PR and/or ping a reviewer. |
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.
changelog: add lint: unbound_return_lifetimes