Lint against unbound lifetimes

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.

5 Likes

I think this would be very useful for FFI work.

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.

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.

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

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.

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.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.