Lint against unbound lifetimes


#1

Unsafe code can easily produce “unbound” lifetimes – lifetimes which can be inferred as anything. These lifetimes are in fact even more powerful than 'static because e.g. &'static &'a T is an illegal type, but an unbound lifetime will successfully promote itself to “only” &'a &'a T if necessary to typecheck.

An unbound lifetime is produced whenever you:

  • deref a raw ptr – let ptr = &*raw_ptr;
  • transmute to a ptr and don’t give an explicit lifetime – transmute::<&T, &U>(foo)

Derefing a raw ptr is certainly inevitable transiently, and creating unbound lifetimes certainly has a practical use (e.g. this is used split_at_mut). However I’m wondering if there are some limited scenarios where it could be linted against.

One trivial case is function signatures where output lifetimes don’t appear in inputs:

fn foo<'a>() -> &'a str

If you write this you’re pretty much always wrong – especially if you’re marking it as safe! In the past we’ve had std-lib errors of this form deriving from unbound lifetimes in struct definitions, and they were promoted to an error-by-default.


#2

I think this would be very useful for FFI work.


#3

So functions can create unbound lifetimes via generics. structs can no longer create them (because that’s an error, right?). What else can create them?

From a syntax::ast perspective, this would look like a Lifetime with a name other than 'static and an empty bounds Vec. Sure enough, when I put your fn foo definition through rustc -Z ast-json, I get (among a lot of other stuff):

"lifetimes": [
  {
    "lifetime": {
      "span": 176093659175,
      "name": {
        "_field0": 63
      },
      "id": 31
    },
    "bounds": []
  }
],

If this is it, we just identify where in the AST this can happen, and the rest is child’s play.

Edit: No, it’s not that easy: If I plug in a fn from_argument(a: &'a str) -> &'a str { a }, I get the same. The difference is that the function above defines the _field0 in its generics, while from_arguments has _field0 in an argument lifetime. So we’d need to create a set of valid argument lifetimes and compare the returned lifetime (if any) against that.


#4

I stumbled across a “valid” safe usecase of unbound lifetimes:

fn get_str<'a>() -> &'a str {
   "hello"
}

But this is basically just a more obfuscated version of 'static, so it’s not clear to me that it matters.


#5

I see no problem with making the lint match this kind of code. If someone really wants to do this, they can always #[allow(unbound_lifetime)]. As an alternative, we could check if the lifetime of every returned expression against 'static and just make it a different static_unbound_lifetime lint. However, as I found out in my option_and_then lint, this is not exactly trivial, with many early return/panic/closure-related corner cases.

Of course, doing this in rustc would still be a breaking change (albeit minor), so we may want to test this out of tree before introducing it. (@Manishearth: another one for rust-clippy, I presume?)


#6

sounds good to me; but note that the set of valid lifetimes might be larger than it seems at first.

bounds is just the : 'b part of 'a: 'b, which is not what we care about.

It might be better if this wasn’t checked from the AST; instead we want to use the TyS from middle and check the Substs to see where the lifetime was defined. I don’t know these variants well enough to know what to look for though.

Edit: On thinking about it, this might actually be better done via the AST.

Check for lifetime params defined in the function space but only used in the return value, once.


#7

Would it be sufficient to warn for cases where an unbounded lifetime leaves an unsafe block? This would still allow the fn get_str<'a>() -> &'a str { "hello" } case that Gankro mentions, while catching the cases where actual bad behaviour might result.