Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

metajack
Copy link

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

@bjorn3
Copy link
Member

bjorn3 commented Dec 17, 2019

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) }
/// }
Copy link
Member

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example note.

if param
.bounds
.iter()
.any(|b| if let GenericBound::Outlives(_) = b { true } else { false })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.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.

Comment on lines 69 to 77
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;
}
}
Copy link
Contributor

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@metajack metajack force-pushed the return-unbound-lifetime branch 2 times, most recently from c3c1f58 to 4744356 Compare December 18, 2019 16:12
@metajack
Copy link
Author

Updated based on review comments and rebased onto master.

@metajack metajack force-pushed the return-unbound-lifetime branch 2 times, most recently from 5a06127 to df9e86c Compare December 18, 2019 16:55
@phansch phansch added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Dec 18, 2019
@ghost
Copy link

ghost commented Dec 21, 2019

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 fn unbound<'a>(x: impl AsRef<str> + 'a) -> &'a WrappedStr, isn't 'a actually an derived from the inputs. Also, in the original PR, I didn't see anyone actually use the term "unbounded lifetime". 🤔

--

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

warning: lifetime is unconstrained
  --> src/re_unicode.rs:62:31
   |
62 |     fn from(m: Match<'t>) -> &'t str {
   |                               ^^
   |

unicase

warning: lifetime is unconstrained
   --> src/lib.rs:298:13
    |
298 | into_impl!(&'a str);
    |             ^^
    |

@dtolnay
Copy link
Member

dtolnay commented Dec 21, 2019

In the example fn unbound<'a>(x: impl AsRef<str> + 'a) -> &'a WrappedStr, isn't 'a actually an derived from the inputs?

No; the bound in x: impl AsRef<str> + 'a means the owner of x is allowed to keep x alive at most as long as 'a if they want. It does not mean x outlives 'a or lives at least as long as 'a. It means actually pretty close to the opposite of that. Deriving an output reference, which is required to live at least as long as 'a, from a value which lives at most as long as 'a, is unsound.

@dtolnay
Copy link
Member

dtolnay commented Dec 21, 2019

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.

@bors
Copy link
Contributor

bors commented Dec 22, 2019

☔ The latest upstream changes (presumably #4930) made this pull request unmergeable. Please resolve the merge conflicts.

@bors
Copy link
Contributor

bors commented Dec 24, 2019

☔ The latest upstream changes (presumably #4885) made this pull request unmergeable. Please resolve the merge conflicts.

@llogiq
Copy link
Contributor

llogiq commented Dec 25, 2019

@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.

@dtolnay
Copy link
Member

dtolnay commented Dec 25, 2019

@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:

  • In regex the 't lifetime is bound in the function argument. I have not looked at the lint implementation but I don't know why it would be triggering on that case.

  • In unicase the 'a lifetime is bound in the function argument, whose type is Self i.e. Unicase<&'a str>.

@llogiq
Copy link
Contributor

llogiq commented Dec 26, 2019

@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.

@flip1995 flip1995 added the A-lint Area: New lints label Jan 8, 2020
@metajack
Copy link
Author

metajack commented Jan 8, 2020

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.

@flip1995
Copy link
Member

flip1995 commented Jan 9, 2020

You can use our integration testing framework for this:

INTEGRATION=serde-rs/serde cargo test --test integration --features integration

runs clippy on serde for example. Note, that this suppresses the output of Clippy. You can get the output, by just adding a println!("{}", stderr) here:

let stderr = String::from_utf8_lossy(&output.stderr);

To test your use case, I also recommend to only enable the lint, you want to test here:

"-Wclippy::pedantic",
"-Wclippy::nursery",

(-Aclippy::all and -Wclippy::unbound_return_lifetimes; Maybe this doesn't work though... #4778)

@metajack
Copy link
Author

metajack commented Jan 9, 2020

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.

@metajack
Copy link
Author

metajack commented Jan 9, 2020

@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.

@flip1995
Copy link
Member

flip1995 commented Jan 9, 2020

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 println!? You could try adding a panic!("{}", stderr). That should get printed.

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.
@metajack
Copy link
Author

@lzutao @bjorn3 I think this is ready to go now.

@ghost
Copy link

ghost commented Jan 10, 2020

I found an old internals thread where this example of a safe unbound lifetime was given:

fn foo<'a>() -> &'a str {
    "string"
}

Does this get linted? I can't check right now because by master toolchain installation is broken.


@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.

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.

@metajack
Copy link
Author

@mikerite For that I think I agree with @llogiq that linting that case seems fine. Thanks for the pointer to this thread.

Copy link
Member

@flip1995 flip1995 left a 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.

@llogiq
Copy link
Contributor

llogiq commented Jan 18, 2020

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;
Copy link
Contributor

@llogiq llogiq Jan 19, 2020

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 {
Copy link
Contributor

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>)>,
Copy link
Contributor

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) }
}

Copy link
Contributor

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!()
Copy link
Contributor

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)?

@bors
Copy link
Contributor

bors commented Jan 20, 2020

☔ The latest upstream changes (presumably #5067) made this pull request unmergeable. Please resolve the merge conflicts.

@flip1995 flip1995 added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Feb 15, 2020
@phansch
Copy link
Member

phansch commented Apr 15, 2020

Hi @metajack, are you still up to finish this PR? Let us know if you have any further questions =)

@metajack
Copy link
Author

Yes, I've just been busy with other stuff. I'll put some time aside soon to get this over the finish line.

@flip1995
Copy link
Member

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.

@flip1995 flip1995 closed this May 25, 2020
@flip1995 flip1995 added S-inactive-closed Status: Closed due to inactivity and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels May 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lint Area: New lints S-inactive-closed Status: Closed due to inactivity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants