Panic on panic(). Is it possible?

What is your opinion?

Ok() => ... ,
Err(e) => {
    println!("Stopping...");
    panic!("The program stopped.").expect("Aaaaaaahhhhh! Panic() does not work. The program continues. ") ;
}

The panic!() macro will eventually call your program's panic handler, which has a signature of fn(&PanicInfo) -> !. .

Your code also doesn't compile because a panic!() expression has the type, !, not Result.

error[E0599]: no method named `expect` found for type `!` in the current scope
 --> src/main.rs:6:40
  |
6 |         panic!("The program stopped.").expect("Aaaaaaahhhhh! Panic() does not work. The program continues. ") ;
  |                                        ^^^^^^ method not found in `!`

2 Likes

I know . It is just an illustration for question ''Should program continue when paanic() fails?''

It is not possible for a panic to "fail". While there are several ways a panic could fail to unwind, those all lead to an infallible abort. (minus OS/cpu bugs, but in that case you have worse things to worry about)

14 Likes

I don't understand what it is that you're asking. Do you care to elaborate?

I think the question is: Can a panic go wrong and make the program coninue, or can you be sure that no command ia executed after panic!.

I would say: Unless you have some unsafe code which leads to undefined behavior (where anything could happen), it is ensured that panic! does not return (and the compiler can use that knowledge).

However panic! does not immediately terminate the thread. There will be clean up work executed (destructors and drop handlers). If these panic, the program usually aborts, if I understand right. In no case will program flow continue after the first panic. (Unless there is undefined behavior caused by some unsafe code, in which case anything could happen anyway.)


struct Zombie {
    last_words: String,
}

impl Drop for Zombie {
    fn drop(&mut self) {
        println!("{}", self.last_words);
        //panic!(); // This will *not* print "This will never happen."
    }
}

fn main() {
    let _zombie = Zombie {
        last_words: String::from("I can walk and talk!"),
    };
    println!("Panicking now...");
    panic!("PANIC!");
    println!("This will never happen.");
}

(Playground)

Output:

Panicking now...
I can walk and talk!

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unreachable statement
  --> src/main.rs:18:5
   |
17 |     panic!("PANIC!");
   |     ---------------- any code following this expression is unreachable
18 |     println!("This will never happen.");
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
   |
   = note: `#[warn(unreachable_code)]` on by default
   = note: this warning originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

warning: `playground` (bin "playground") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 1.35s
     Running `target/debug/playground`
thread 'main' panicked at 'PANIC!', src/main.rs:17:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

1 Like

That depends on definition of "failing".

This example will never finish unwinding and never cause an abort (but only be killed by Playground due to too-long execution time):

struct Invincible;

impl Drop for Invincible {
    fn drop(&mut self) {
        loop {
            std::thread::sleep(std::time::Duration::from_millis(500));
            println!("A mere panic will never kill me, Mwahahaha!")
        }
    }
}

fn main() {
    let _invincible = Invincible;
    println!("Panicking now...");
    panic!("PANIC!");
}

(Playground)

Output:

Panicking now...
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!
A mere panic will never kill me, Mwahahaha!

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/playground`
thread 'main' panicked at 'PANIC!', src/main.rs:15:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
/playground/tools/entrypoint.sh: line 11:     9 Killed                  timeout --signal=KILL ${timeout} "$@"

1 Like

Another weird example, which is certainly not idiomatic Rust:

struct Restarter;

impl Drop for Restarter {
    fn drop(&mut self) {
        if std::thread::panicking() {
            println!("Oh oh, a panic, let's start over!");
            main();
        }
    }
}

fn main() {
    let _restarter = Restarter;
    if std::thread::panicking() {
        println!("... but we're still panicking.");
    }
    println!("Hello World!");
    panic!("PANIC!");
}

Output:

Hello World!
Oh oh, a panic, let's start over!
... but we're still panicking.
Hello World!

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/playground`
thread 'main' panicked at 'PANIC!', src/main.rs:18:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at 'PANIC!', src/main.rs:18:5
stack backtrace:
   0:     0x563dced5a500 - std::backtrace_rs::backtrace::libunwind::trace::h32eb3e08e874dd27
                               at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/../../backtrace/src/backtrace/libunwind.rs:93:5
…
thread panicked while panicking. aborting.
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Aborted                 timeout --signal=KILL ${timeout} "$@"

6 Likes

I like this thread a lot. I would have bet some money on "uncatched panic will abort your program in some sense", and now I learn that - of course - it is quite easy to make whatever I want happen

Not exactly.

What @jbe is pointing out is that your panic handler (the thing we call as part of the panicking process) can do anything it wants as long as it satisfies the signature, fn(&PanicInfo) -> !.

By evaluating to the never type, !, we say that the function will never "return" in the normal sense of the word (i.e. your program can continue executing the next line of code).

Instead, it must "diverge". There are a couple ways a function can diverge, but here are some I can think of off the top of my head,

  • Aborting the process
  • Immediately kill the current thread (but let the process continue)
  • Unwind the stack and kill the current thread (the "normal" behaviour)
  • Get stuck in some sort of infinite loop
  • Using setjmp() and longjmp() to do a goto across function boundaries (super unsafe!)

When a function is marked as divergent, LLVM will deliberately insert an invalid instruction after the instruction for calling the function to make sure the process aborts.

For example, let's look at this silly piece of code:

pub fn never_returns() -> ! {
    loop {}
}

fn main() {
    never_returns();
}

(playground)

These are the instructions it generates:

playground::never_returns:
	jmp	.LBB8_1

.LBB8_1:
	jmp	.LBB8_1     <--- jumps back to itself (infinite loop)

playground::main:
	pushq	%rax
	callq	playground::never_returns
	ud2             <--- your process will always abort when it tries 
                         to execute this instruction

That means even if our never_returns() function could somehow return, the process would abort because we'd execute ud2 immediately afterwards.

Now mentally replace never_returns() with your panic handler, and you'll see that it's impossible to continue the normal flow of execution after a panic!().

1 Like

Yes, I understand that. My point is more: I'm always surprised what kind of freedom is implied by low-level programming. I guess it just means "everything is possible". And I'm so used in thinking limited that this surprises me over and over again. And I'm quite happy about this :smiley:
So, please continue, compose simply ideas in simply ways to achive surprising results

This is only allowed if the stack is never deallocated and in that case is effectively equivalent to getting stuck in an infinite loop. Deallocating the stack without unwinding is not allowed as that would make stack pinning unsound.

3 Likes

True, but I'm sure someone could find a way to use low-level APIs to do it anyway.

Interestingly, I tried calling pthread_exit() in the playground directly and we unwound the stack like normal, so it looks like pthread_exit() is built on top of an exception which can't be caught with std::panic::catch_unwind(). That would line up with this StackOverflow answer about catch (...) in C++, too.

1 Like

pthread_exit uses forced unwinds. It is UB to do a forced unwind through a stack frame containing locals that need to be dropped. See https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html#forced-unwinding

2 Likes

If what you want is for the program to abort on panic, instead of unwiding, just add panic = "abort" to your Cargo.toml.

This is documented in the official rust book: Unrecoverable Errors with panic! - The Rust Programming Language

1 Like

No, it's a bit more horrifying. Glibc looks for libgcc.so and if it can find it just calls _Unwind_ForcedUnwind.

There are no exception to catch, stack is just unwond forcibly.

2 Likes