Prohibit the use of unwrap() and increase Panics privacy!

no_panic is extremely invasive and only works on a single function at a time, instead of being able to assert that all crates in a program are panic-free. It's also a bit of a hack / leaky abstraction (e.g. it can break debug builds).

1 Like

I'd like to contribute my arguments why something like no_panic should be in the rust compiler itself (sorry for the long comment):

I recently had the goal (in a security critical section of code) of making absolutely sure I've handled all error cases, so I tried using the amazing no_panic crate. Unfortunately one of my dependencies (a wrapper around a CPU instruction that returns an error code) panics if the error code returned by the instruction is outside of the range specified in the Instruction manual. Which is exactly what should happen. Unfortunately that introduces a panic path that cannot be optimized away and thus no_panic complains.

Although there is an even more hacky (imo) way to make no_panic ignore panics in a function, I've found it really hard to use (especially if you still want the panic in the final release). I'd have needed conditional compilation or a custom profile for checking for no_panic, which is (a) difficult, (b) annoying and (c) error prone, as the no_panic change/implementation may result in different optimizations (e.g. tree shaking) which could re-introduce panics. And in this case I wasn't even able to use that because the types I was using did not work with that workaround.

In the end I gave up, and looked at the assembly output to check manually that my code did not panic except for that dependency call (though I'm still not 100% sure it is panic free). Note that that was one function out of multiple that I wanted to annotate with no_panic.

The issue of programmers not adding panics could probably be solved this way, too: If you add something like a #[allow_panic] macro in front of functions or (explicit) panics you want to keep at runtime you have an easy way of doing that. While the resulting code is not guaranteed to be panic free, you could use that to ease the transition to panic-free code (e.g. when a dependency contains a panic) or if you're 99.9% sure it won't panic. The use of that macro could for example be forbidden via linting or ignored on analysis if you really want to ensure you have no panic anywhere.

I have looked into static analysis a bit (mainly prusti), but that has other problems with more complex language features.

Correct me if I'm wrong, but I believe that the compiler has a significantly higher chance at making such exceptions (for the purpose of analyzing panics assume this function does not panic, even though the resulting binary contains a panic) possible.

This, together with the (as @kpreid mentioned already difficult task of writing panic-free code in the first place makes me think this functionality (if even possible from a technical standpoint) should be in rustc itself.

Personally, I think "just" having a no-panic profile for the entire crate won't be sufficient, because writing panic-free code is so hard. Some crates might want this but often (especially when you want to start using it) you only want it checked on some parts of the code.

As for the hack/leaky abstraction and breaking in debug builds: True, but I don't see a way around that unless the compiler can use IR, MR or LLVM IR (whatever that was called) to "know" more about the function and use that for panic analysis. Even then you'd likely want a no_panic_in_release or similar for those cases.

TLDR

I think it would be insanely hard to get entire crates and dependencies panic-free (especially when considering problems with breaking changes). But it should be as easy as possible (hence why I like no_panic even though I was unable to use it) to mark individual functions (or the entire crate if desired) as requiring panic-free (at least for --release). With an easy (and working!) way of excluding a called function from that check, which may have a valid reason to panic.

3 Likes

Looking at MIR and LLVM IR also have lot of problems. Looking at LLVM IR in particolar ties the compiler to one specific backend (what about Cranelift and GCC?), and more in general looking at code after optimizations is very likely to break with new compiler updates.

True, I'd also prefer if it was done at a higher level. But I think in many cases proving something will not panic requires optimizations that (currently) happen on MIR or LLVM IR. I do not know how useful something like no_panic would be to non-trivial code if it would be unaware of optimizations done by the compiler.

So it would likely be able to prove a lot less, or require some kind of analysis/optimization on the higher level representations, which could mean something proven as non-panicking could end up with a panic path in the resulting binary if those do not match (although since the compiler literally proved it is impossible to reach it wouldn't make a real difference).

In my opinion having a best-effort no_panic that may break with new compiler versions is better than not having it at all. As for stability that would then just be guaranteed to still exist in the language, not guaranteeing that it will be able to prove it in future compiler versions, too, as there is no stability guarantee in compiler optimizations either. If having a dependency break when compiling is a concern it could either be an op-in or only return warnings instead of compiler errors (or have it be configurable by the dependency).

Is this something that a proposed effects system could be used for?

I think an effect system would help. But I don't think the proposed one would. Because we'd need an unwind effect and all existing API would have to be rewritten to account for that because it doesn't automatically infer/derive the applicable effects, it requires explicit declarations for each trait or function.

In many cases we want something akin to mem::needs_drop() to get an inferred effect (or its absence) rather than a declared one.

Interesting rabbit hole, especially the part about Effect states.

I think those two are orthogonal to, but benefiting from each other. We never have two implementations of a function, one that panics and one that doesn't. It is just a property of the implementation that may or may not change in the future and that may or may not be provable by the compiler. From what I've read on that topic so far it is primarily useful (from the perspective of the programmer, not the compiler internals) when using traits, async and others where it does make a difference in how the function is called, where you do need two (slightly) differing implementations.

I don't know how having an exception (the word, not the Exceptions in other languages or Panics) for a function/expression that does (and is supposed to) panic could be done with this effect system for example. As the function is still technically capable of panicking. I see it more like unsafe but kind-of inverted: You have a keyword/attribute/whatever you can add to a function, which tells the compiler you want a compiler error if it can panic (similar to how you get an error when calling unsafe in a function outside an unsafe block or function). And you have a keyword/attribute/whatever you can add around a function that the compiler couldn't figure out is safe (similar to how the unsafe block works) (I hope that makes sense).

So you may even need two effects: One for the actual behavior "this definitely does not panic, there is no code in it that can" and one for the behavior you actually want to prove: "there is no panic, except for where I told you to ignore it, but at runtime this may still panic". That system may make use of the effect states, but I think most of it requires a bunch of code separate from the effect system.

Unless I'm missing something on keyword-generics that is internal to the compiler and not directly visible to the programmer.