Enforcing no-std and no-panic during build

In the discussion on the linux mailing list: https://lkml.org/lkml/2021/4/14/1099 https://lkml.org/lkml/2021/4/14/1130

It was mentioned that they need to operate with a subset of the language that cannot panic. (And also disabled automatic overflow in arithmetic). This seems fairly easy to achieve with flags and conditional compilation.

From an ecosystem point of view this creates a situation similar to no-std today. Crates are tagged and documented as being no-std in crates.io so that people can tell if they can be used in their context. It seems likely that we will eventually have crates being marked as no-panic to indicate they they are no-std and also compile without the need for code to support panicking. Obviously such crates can still be used by applications that don't care about such things.

This introduces a problem similar one that exists for no-std crates today. It's easy to make a mistake and call a function which is not available and cause downstream crates' builds to fail.

The problem is that during development std, panic, etc are available because they are needed for tests. Hence even if the code is made to accidently depend on them it will compile.

It would be extremely useful to have a way to fail a build of a crate which for example declares itself no-std and ends up linking against std and, assuming such an annotation is added, declares itself no-panic but ends up linking against code to panic. This way rather than failing a downstream build after the crate has be published, the bug is caught early and the developer sees an error as soon as they type the offending line.

3 Likes

When compiling for no_std and in release mode, you can enforce that panic is never called with https://crates.io/crates/no-panics-whatsoever. It's a bit hacky, but it will error at compile time if any panics are present in the binary.

I'd be very interested in this as well.

I'm familiar with crates like dtolnay's no-panic and japaric's panic-never, the latter of which is being suggested as a convention or requirement for Rust embedded code, but...

It really seems like the sort of thing that belongs in rustc proper, as opposed to some sort of external hack. It would be nice to say, for some degree of confidence, that a whole program composed of several crates is panic-free, instead of crates having to opt into it on a crate-by-crate basis.

I also imagine that if the compiler knows a program will never panic, it could automatically make use of optimizations which aren't possible in panicking code.

The only way I know how to do that today is using const fn, which presently entails writing panic free code because const_panic isn't stable, but that almost seems like an accidental way of ensuring code is panic-free, and it looks like const_panic will be stable soon. That's actually great and I actually want to use const_panic in other contexts, and perhaps it's okay for otherwise "panic free" code to have const_panic code because it will only panic at compile time.

3 Likes

FFI is another situation where panicking gets complicated. I’m currently working on reducing panicking in rustls (motivated in part by the in progress C API that we are building) and it would be great to have “official” support for this in the language/tooling.

I suppose one of the ergonomic challenges here is that we might want to provide both fine-grained (this function doesn’t panic) and more coarse-grained annotations (functions defined in this module are not allowed to panic).

I think this could then also map to LLVM’s nounwind function attribute?

3 Likes

You can still panic in a const function in stable by dividing an integer by zero.

That would be lovely. Combined with oom=panic, it could also be used to enforce that libraries use fallible allocations.

I think it'd be very useful as a function annotation that checks, recursively, whether anything invoked in this function could panic.

#[no_panics_please]
fn foo() {
    println!("hi"); // compile error: println can panic!
}
4 Likes

I suppose a further question is: is not panicking part of the semver contract? That might inform whether only functions that are explicitly tagged #[no_panic] can be called by other functions that are #[no_panic], or whether the compiler is allowed to infer that some functions cannot panic.

1 Like

If rust code isn't allowed to panic, would C code not be allowed to bug or oops? Rust are not meant for errors that can be handled in normal way. Result is meant for this. Panics are meant for logic bugs and are meant to terminate at leaat the current thread (aka an oops in the linux kernel). std::process::abort is meant for when it is impossible to safely recover from the error/bug at all (aka a kernel panic). What would make sense is to have a method to warn/error on operations that can implicitly panic like array indexing. Ensuring that a program doesn't panic is IMHO equivalent to either proving the absence of bugs in the program or just silently returning bogus results in case of bugs. If there is a sensible way to handle an error that doesn't involve discarding what you are currently doing, be it handling a syscall, an http request or something else, you should not panic.

6 Likes

That's a good question. I guess it'd have to be explicit #[no_panic] on everything, because otherwise it'd be too easy to break someone at distance.

It would be a bit spammy, because you might want that on all applicable functions, but I suppose this problem can be solved later, e.g. with another feature that sets attributes en masse for all functions in a scope. #[must_use] is similarly a noisy default (compared to Swift's "must use" enabled by default and an attribute to mark a result as discardable), so I'd like ability to say e.g. "everything in this impl block is must_use, inline, and no_panic".

C code is already not allowed to unwind through Rust's FFI boundary, so in a literal sense C can't panic either.

In a wider sense, I see two differences:

  • in C it's just more customary to handle failures in not-world-ending ways. Because you can't rely on a panic keeping things safe, you just have no choice but to handle that problem before it happens, much more carefully and proactively than you'd have to in Rust. And this is typically done by returning an error. When abort() is used it's IMHO more intentional when it's the right thing to do for the whole program, not just an oversight caught by some leaf function that doesn't know what to do.

  • even in like-for-like situations, it's just an opportunity for Rust to do better than C can. In C you can't enforce that every array index and null pointer is checked (neither static analyzers nor runtime sanitizers give a guarantee of catching every case), but with a no-panic Rule you could forbid every lazy .unwrap() and enforce that Rust uses .get(x)? everywhere instead of [x].

2 Likes

I wonder what core/std would look like if you tree-shook all (most?) potentially panicking code paths out. It'd definitely be overly conservative (as much of the library fairly uses [ix] even if the index is known inbounds, and lets the optimizer deal with the panicking branch), but I think it'd still be a useful experiment to see what is already done in a 100%-won't-panic manner. Whatever the outcome, even if it's barely any functionality, would be a interesting and potentially useful data point.

(That said, I think the use case of literally implementing the kernel is about the best possible argument for #![no_core], because the constraints in kernel code are so different from even safety-critical microcontrollers.)

The general way you'd go about tree-shaking out potentially panicking code paths is theoretically simple: remove panic!, then recursively remove everything that uses the removed functionality.

Rust panics seem wholly unwelcome in the Linux kernel. Categorically it's considered (at least by Linus himself) a "fundamental issue" apparently.

And honestly I can sort of understand it. The last thing you want when developing kernel code is to bump up against language-specific (anti)features which are standing in your way. I'm not saying that Rust panics are an anti-feature btw, more that in certain contexts it can be considered as such, as indicated by Linus' response.

1 Like

An interesting compromise might be that the compiler will infer it for crate-local functions, but will only rely on the annotation across crate boundaries.

2 Likes

That was something where panics arguably shouldn't have been used at all as BUG_ON shouldn't have been used either. It was not a case of something has gone horribly wrong and there is no sensible way to handle to other than to restart. OOM handling in the kernel shouldn't use panic and the compiler builtin calls should have not been emitted at all. What I am saying is that panics should be used for cases where you can't recover aka the exact same cases where currenty BUG_ON is currently being used inside the kernel. I do agree that it may be a good idea to make it harder to accidentally introduce a potential panic through for example [], but explicit panics should not be completely forbidden when BUG_ON isn't forbidden for the respective error case either. #[no_panic] would also prevent crashing in the face of genuine corruption that has to abort the system to prevent an attacker from causing harm.

4 Likes

In theory you could encode panic worthy bugs in a Result's Err type (as an enum perhaps). Nothing prevents you from plumbing it up all the way to the top, right? I won't say it's ergonomic, but it seems possible... and it might be less work than trying to prevent most but not all panics.

1 Like

It would have to be done manually, e.g.

if data.is_corrupted() {
    abort();
}

Option and Result could also be extended with a .unwrap_abort() method that can be used even in a #[no_panic] function.

I think one could just use .unwrap_or_else(|| abort()), given that the abort function is likely to be customised in this case.

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