Per target panic setting

I have a crate producing both a [[bin]] that runs as a server process and
a [lib] target with shared code for clients. The primary use of the lib is
as cdylib linked into various C and C++ binaries.

Currently the crate builds with panic = "abort" which is the desired behavior
for the server binary, but rather unsuitable for the [lib] used by
clients as policing potential panics in dependencies is way out of scope.
Thus I would like to switch the library – and only the library! – to panic = "unwind"
and guard the FFI boundary with std::panic::catch_unwind. (Note that this is not
intended as a discussion of the merits of unwinding!)

Browsing the Cargo docs I couldn’t find a way of specifying the panic behavior
per target. There’s profiles sure but they apply to the crate as a whole;
overrides
too it seems can only be specified with crate granularity.

Anyone got a solution that doesn’t involve splitting the crate?

What is the advantage of setting panic = "abort" only for the server binary? panic = "abort" is normally used when either unwinding is not supported on a target at all (not the case here) or as optimization (for real benefits you need to compile everything with panic = "abort")

What specific cases do you encounter when .unwrap() shouldn't have been used.

What is the advantage of setting panic = "abort" only for the
server binary?

A library call should not abort the process it’s being called
from. Normal C style exception handling à la longjmp(3) isn’t
an option with Rust so unwind it is.

panic = "abort" is normally used when either unwinding is not
supported on a target at all (not the case here) or as
optimization (for real benefits you need to compile everything
with panic = "abort")

Aborting really doesn’t have any downsides for an executable;
smaller binaries, faster resource cleanup, it’s in line with what
other programs do etc. You maybe lose the builtin backtraces but
those are more of a gimmick anyways considering coredumps and gdb
are more suitable for debugging.

Thanks for your response anyways, I’ve edited the question to be
more focused.

I assume that most code is in the library. This means that using panic = "abort" only for the executable doesn't have much effect, as the library still has the landing pads necessary for cleanup and the unwind info for it is also included in the final executable. In addition panic = "abort" doesn't omit the dwarf unwind info. It only limits it to the parts necessary for a backtrace, omitting the parts necessary for cleanup.

panic!() should only be used for bugs in your code. catch_unwind is normally used in server code to for example respond with a 500 internal server error just for the request that caused the panic while keeping the server able to process further requests without requiring a restart.

AFAIK profiles are for the entire workspace, not even per-crate. So you'll be forced to completely split the cdylib and the binary into two separate crates, and they can't even be in the same workspace.

2 Likes

I assume that most code is in the library.

The library is the smallest part as it is mostly concerned with
implementing serialization / deserialization for IPC. (Clients
talk to the server using Unix sockets.)

This means that using panic = "abort" only for the executable
doesn't have much effect, as the library still has the landing
pads necessary for cleanup and the unwind info for it is also
included in the final executable. In addition panic = "abort"
doesn't omit the dwarf unwind info.

Those .eh_frame and .eh_frame_hdr tables are enormous
actually even if built with abort.

It only limits it to the parts necessary for a backtrace,
omitting the parts necessary for cleanup.

With “cleanup” do you mean destructors? That is not something
that should be stripped then.

Asking more out of curiosity, is there any documentation as to
how Rustc uses those .eh_* sections?

In general, if you say that a library is panic=unwind, it will require that the final binary it's included in is also panic=unwind. So while you can't set this up in Cargo because there's no direct setting, it also just would not work, because when you tried to include the library in the binary, you'll get an error.

AFAIK profiles are for the entire workspace, not even
per-crate. So you'll be forced to completely split the cdylib and
the binary into two separate crates, and they can't even be in
the same workspace.

We’re not using workspaces, just plain cargo with an internal
registry and RPM.

It would be great though if the panic behavior could be
configured for each build output separately so it applies to all
dependencies recursively regardless of whether it involves
recompiling everything.

You can use strip to remove them.

Cleanup is running destructors when unwinding. Destructors during regular execution are preserved.

.eh_* was introduced for unwinding in C++. Rust works pretty much the same.

libstd is always built with panic=unwind even when any other crate is built using panic=abort. If any crate is panic=abort rustc will treat it as if all crates are panic=abort apart from the fact that the panic=unwind crates have unnecessary unwind info.

To elaborate on this. The .eh_frame_hdr section is a lookup table into the .eh_frame section. The .eh_frame section contains information about how to restore all registers when unwinding for each function. This always exists. In addition it optionally contains a personality function and LSDA (language specific data area) During unwinding the personality function is called to determine if it needs to perform cleanup or if it needs to stop unwinding (catch_unwind). For this is looks up a language specific piece of data pointed to by the LSDA. In practice for Rust this is in the .gcc_except_table section as that is where LLVM stores it. These parts only exist for panic=unwind.

You can use strip to remove them.

A lot of this would originate in stdlib, right? That’s around 96 %
of the binary size when built with aborting behavior.
(EDIT: Bad
measurement, I also passed --as-needed.)

Cleanup is running destructors when unwinding. Destructors
during regular execution are preserved.

Thanks for clearing that up!

Wow, that is a lot. I wouldn't expect it to be that big under any circumstance.

Disregard that, that measurement was done in haste five minutes before I clocked out at work. I forgot to consider I also passed --as-needed. Sorry for the noise, I’ll check the correct values tomorrow.

Consider an HTTP service made with Tokio. If some edge case which wasn't properly accounted for causes a future to panic when handling a client, the thread will unwind and run destructors such as unlocking resources, the future runtime can catch and log the panic, then the thread will continue working and the other connections on the same server can hopefully be unaffected.

Wow, that is a lot. I wouldn't expect it to be that big under any circumstance.

You’re right, strip was being overly aggressive. The size of those sections is
neglibible regardless of the panic behavior. Still, abort builds end up 75 %
the size of unwind ones.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.